Java集合类详解三HashMap
1.什么是HashMap
HashMap实现了Map接口,是一个用于存储键值对(key-value)的集合,每一个键值也叫做Entry,这些键值对分散存储在一个数组中,这个数组就是HashMap的主干,Jdk1.8后由数组+链表变为数组+链表+红黑树。
2.方法特性
2.1初始容量
1 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 |
2.2最大容量
1 | static final int MAXIMUM_CAPACITY = 1 << 30; |
2.3默认加载因子
1 | static final float DEFAULT_LOAD_FACTOR = 0.75f; |
2.4链表转换为树的阙值
1 | static final int TREEIFY_THRESHOLD = 8; |
2.5树转化成链表的阙值
1 | static final int UNTREEIFY_THRESHOLD = 6; |
2.6键值对数量大于64才会发生转换
1 | static final int MIN_TREEIFY_CAPACITY = 64; |
2.7hash方法
1 | //计算对应的位置,key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。 |
举例:hashcode:十进制的3029737,二进制的101110001110101110 1001。
假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111
把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
为什么长度16:反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
2.8put方法
1 | public V put(K key, V value) { |
步骤
1.判断数组是否为空,如果是空,则创建默认长度位 16 的数组。
2.通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个。
3.如果有元素,说明 hash 冲突了,则再次进行 3 种判断。
1.判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新e的value,也就是替换操作。
2.如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。
3.如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。
4.最后,如果这三个判断返回的 e 不为null,则说明key重复,则更新key对应的value的值。
5.对维护着迭代器的modCount 变量加一。
最后判断,如果当前数组的长度已经大于阙值了,则重新hash。
2.9get方法
1 | public V get(Object key) { |
3.扩容
1 | final Node<K,V>[] resize() { |
1.判断容量是否大于0,如果大于0,并且容量已将大于最大值,则设置阀值为 int 最大值,并返回,如果老的容量乘以 2 小于最大容量,且老的容量大于等于16,则更新阀值。也就是乘以2.
2.如果老的阀值大于0,则新的容量等于老的阀值。注意:这里很重要。还记的我们之前使用new 操作符的时候,会设置阀值为 2 的幂次方,那么这里就用上了那个计算出来的数字,也就是说,就算我们设置的不是2的幂次方,HashMap 也会自动将你的容量设置为2的幂次方。
3.如果老的阀值和容量都不大于0,则认为是一个新的数组,默认初始容量为16,阀值为 16 * 0.75f,也就是 12。
4.如果,新的阀值还是0,那么就使用我们刚刚设置的容量(HashMap 帮我们算的),通过乘以 0.75,得到一个阀值,然后判断算出的阀值是否合法:如果容量小于最大容量并且阀值小于最大容量,那么则使用该阀值,否则使用 int 最大值。
5.将刚刚的阀值设置打当前Map实例的阀值属性中。
6.将刚刚的数组设置到当前Map实例的数组属性中。
7.如果老的数组不是null,则将老数组中的值重新 散列到新数组中,如果是null,直接返回新数组
4.遍历方法
4.1采用keySet()+for循环
1 | Map<String,String> map=new HashMap<String,String>(); |
4.2采用EntrySet()+Iterator进行遍历
1 | Iterator< Map.Entry<String,String>> |
4.3直接采用EntrySet+for增强进行遍历
1 | for(Map.Entry<String,String> |
4.4取出所有value的值,但是不能取出KEY值
1 | for(String value:map.values()) |
5.总结
底层实现是 链表数组,JDK 8 后又加了 红黑树
实现了 Map 全部的方法
key 用 Set 存放,所以想做到 key 不允许重复,key 对应的类(一般是String)需要重写 hashCode 和 equals 方法
允许空键和空值(但空键只有一个,且放在第一位,知道就行)
元素是无序的,而且顺序会不定时改变每次扩容后,都会重新哈希,也就是key通过哈希函数计算后会得出与之前不同的哈希值,这就导致哈希表里的元素是没有顺序,会随时变化的,这是因为哈希函数与桶数组容量有关,每次结点到了临界值后,就会自动扩容,扩容后桶数组容量都会乘二,而key不变,那么哈希值一定会变
插入、获取的时间复杂度基本是 O(1)(前提是有适当的哈希函数,让元素分布在均匀的位置
遍历整个 Map 需要的时间与数组的长度成正比(因此初始化时 HashMap 的容量不宜太大)
两个关键因子:初始容量、加载因子
HashMap不是同步,HashTable是同步的,但HashTable已经弃用,如果需要线程安全,可以用
参考链接:https://blog.csdn.net/qq_36711757/article/details/80394272
https://blog.csdn.net/qq_38182963/article/details/78942764