hashmap重要变量
源码中定义了很多常量,有几个是特别重要的。
- DEFAULT_INITIAL_CAPACITY : Table数组的初始化长度: 1 << 4,即 2^4=16(这里可能会问为什么要是2的n次方?)
- MAXIMUM_CAPACITY :Table数组的最大长度: 1<<30, 即 2^30=1073741824
- DEFAULT_LOAD_FACTOR : 负载因子:默认值为0.75。 当元素的总个数>当前数组的长度 * 负载因子。数组会进行扩容, 扩容为原来的两倍 (这里可能会问为什么是两倍?这个问题与上述2的n次方相关联)
- TREEIFY_THRESHOLD : 链表树化的阈值,默认为8,表示载一个node(table)节点下的值的长度大于8时就会转变为红黑树
- UNTREEIFY_THRESHOLD : 红黑树链化阈值,默认为6,表示载一个node(table)节点下的值的长度小于6时就会转变为链表
- MIN_TREEIFY_CAPACITY = 64 :最小树化阈值,当Table所有元素超过该值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)。
构造器
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
复制代码
空参构造器中对属性loadFactor(加载因子)进行了赋值操作,初始值为16。
注意: JDK8中创建完HashMap对象后并没有立即创建长度为16的数组。
最显而易见的区别
- JDK 1.7 : Table数组+ Entry链表;
- JDK1.8 : Table数组+ Entry链表 ==> 红黑树;(可能会问为什么要使用红黑树?)
为什么使用链表+数组?
因为为了避免hash冲突的问题。由于我们的数组的值是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。
我⽤LinkedList代替数组结构可以吗?
可以
那既然可以使用进行替换处理,为什么又偏偏使用到数组呢?
因为用数组效率最高! 在HashMap中, 定位节点的位置是利用元素的key的哈希值对数组长度取模得到。 此时,我们已得到节点的位置。显然数组的查找效率比LinkedList大(底层是链表结构)。
ArrayList,底层也是数组,查找也快,为啥不⽤ArrayList?
因为采用基本数组结构,扩容机制可以自己定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率高。⽽ArrayList的扩容机制是1.5倍扩容。
当两个对象的 hashCode 相同会发生什么?
因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。
你知道 hash 的实现吗?为什么要这样实现?
JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 未实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
为什么要用异或运算符?
保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。
为什么是2的幂次?与数字2的关系
首先为什么长度要是2的n次方?
这依然关系到hash冲突。
简单来说,就是如果是2的幂,就能减少hash冲突的出现
因为这个key存放到数组中哪个位置,,源码中有个按位与来判断,即 hash & (length - 1) ,如果是2的幂,那么这个减一的操作会让最后4个位数都是1111,进而保证哈希值能分布早0-15之间。如果不是2的幂的话,有可能出现某个哈希桶(可以理解为数组中某个位置)是一直为空的,不能完全利用好数组。
注意: hash值是个32位的int型
那么如果不是容量不是2的幂呢?
源码中有个静态方法 private static int roundUpToPowerOf2 ,这个方法会将容量扩充为2的幂。
int rounded = number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY
: (rounded = Integer.highestOneBit(number)) != 0 ?
(Integer.bitCount(number) > 1) ? rounded << 1 : rounded
: 1;
复制代码
流程图如下:
hashmap为什么是二倍扩容?
容量n为2的幂次方,n-1的二进制会全为1,位运算时可以充分散列,避免不必要的哈希冲突。所以扩容必须2倍就是为了维持容量始终为2的幂次方。
阶段总结
Hashmap的结构,1.7和1.8有哪些区别
- JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法,那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
- 扩容后数据存储位置的计算方式也不一样:hash & (length-1)
- JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数