一、HashMap 的存储结构键值均可为 null
JDK7 的 HashMap
JDK7 的 HashMap 的存储结构其实就是哈希表的存储结构(由数组与链表结合组成,称为链表的数组)。如下图:
如上图,HashMap 中元素存储的形式是 key-value 键值对,即 Entry 对。一个长度为 16 的数组中,每个元素存储的是一个链表的头结点。这些元素是按照什么样的规则存储到数组中呢?一般是通过 hash(key.hashCode())%len 获得,也就是元素的 key 的哈希值对数组长度取模得到。
上述哈希表中,12%16=12;28%16=12;108%16=12;140%16=12。所以 12、28、108 以及 140 都存储在数组下标为 12 的位置。
JDK8 的 HashMap
当链表长度超过阈值(TREEIFY_THRESHOLD) 8,table 的长度大于 64如果小于 64,就通过扩容的方式来解决,避免红黑树结构化
链表转换为红黑树。
区别:
1️⃣JDK7:new HashMap() 时,底层创建 size 为 16 的数组Entry[]
。
2️⃣JDK8:首次调用 put() 时,底层创建 size 为 16 的数组Node[]
。
3️⃣JDK7 底层:数组+链表。JDK8 底层:数组+链表+红黑树。
当数组某个索引位置的元素以链表形式存在的数据个数 >8 且当前数组的长度 >64 时,该索引位置上的所有数据改为使用红黑树存储。
4️⃣新节点插入到链表的顺序不同:JDK7---头插法因为插入头部效率高,不需要遍历整个链表
,JDK8---尾插法。
//JDK7 的头插法:addEntry 的 createEntry 方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
道理很简单,就是创建一个 Entry,该 Entry 的 next 是 e(参照new Entry<>(hash,key,value,e)的实现),这个 e 就是之前的头结点,然后把当前 table 的位置放上新建的 Entry,也就是插入在链表的头部了。
//JDK8 的尾插法,putVal 的一段代码:
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
...
JDK8 链表大于 8 的时候要树化,需要遍历算该链表的长度,直接把数据放到后面,这样方便,减少了把数据移动到头数组位置这一步,效率也提高了。而且,JDK7 中出现的 HashMap 死循环 bug 不会有了,因为这里是只是平移,没有调换顺序。
5️⃣JDK8 的 hash 算法有所简化
JDK7 默认初始化大小 16,加载因子 0.75。如果传入了 size,会变为大于等于当前值的 2 的 n 次方的最小的数。
// Find a power of 2 >= toSize
int capacity = roundUpToPowerOf2(toSize);
为什么是 2 的幂次方?
因为确定下标位置的 indexFor 方法,h&(length-1)。length 是 2 的次方,那么 length-1 总是 0001 1111 等后面都是 1 的数,h& 它之后,其实就相当于取余,与的效率比取余高,所以用了这种方式达到高效率。下面是 JDK7 的 indexfor 的实现:
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
二、JDK7 的 HashMap 底层实现原理
put方法实现原理
HashMap map = new HashMap();
在实例化后,底层创建了 size 为 16 的一维数组 Entry[] table。
map.put(key1,value1);
首先,调用 key1 所在类的 hashcode() 计算 key1 的哈希值,此哈希值经过某种算法以后,得到在 Entry 数组中的存放位置。
1️⃣如果此位置上的数据为空,此时的 key1-value1 添加成功。【情况一
】
2️⃣如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据以链表形式存在
),比较 key1 和已经存在的一个或多个数据的哈希值:
①如果 key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。【情况二
】
②如果 key1 的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,然后调用 key1 所在类的 equals(key2) 继续比较。如果返回 false:此时 key1-value1 添加成功。【情况三
】
如果返回 true:使用 value1 替换 value2。【情况四
】
说明:【情况二
】和【情况三
】此时 key1-value1 和原来的数据以链表的方式存储。在不断的添加过程中,当超出临界值(map 中元素总数超过 Entry 数组的 75%),且要存放的位置非空时,为了减少链表长度,元素分配更均匀,触发扩容操作。默认的扩容方式:扩容为原来容量 size 的 2 倍,并将原有的数据复制过来,size 一定为 2 的 n 次幂。扩容针对整个 Map。每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入。插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)。
hashCode 是定位,确定存储位置;equals 是定性,比较两者是否相等。
get方法实现原理
与 put 类似,会首先调用 Key 值中的 hashCode(),用于获取对应的 bucket 的下标值,找到 bucket 的位置后,再通过 key.equals() 找到对应的键值对,从而获得对应的 value 值。
三、JDK7 的 HashMap 源码分析
属性信息
/**
* The default initial capacity - MUST be a power of two.
*/
//Hashmap的初始化大小,默认容量为 16,也可以构造时指定
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
//HashMap是动态扩容的,最大支持容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
*/
//默认的扩容因子,这个值可以通过构造修改
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* An empty table instance to share when the table is not inflated.
*/
//空数据,默认构造的时候赋值为空的Entry数组,在添加元素的时候
//会判断table=EMPTY_TABLE ,然后就扩容
static final Entry<?,?>[] EMPTY_TABLE = {};
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
//表示一个空的Hashmap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
/**
* The number of key-value mappings contained in this map.
*/
//Hashmap的大小
transient int size;
/**
* The next size value at which to resize (capacity * load factor).
* @serial
*/
// If table == EMPTY_TABLE then this is the initial capacity at which the
// table will be created when inflated.
//初始容量,如果构造传入的值是12,那么这个值就是12,如果没有传入就是默认的容量
//DEFAULT_INITIAL_CAPACITY=16
//扩容的阈值
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
//扩容因子,没有传入就使用默认的DEFAULT_LOAD_FACTOR = 0.75f
final float loadFactor;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
//数据操作次数,用于迭代检查修改异常
transient int modCount;
/**
* The default threshold of map capacity above which alternative hashing is
* used for String keys. Alternative hashing reduces the incidence of
* collisions due to weak hash code calculation for String keys.
* <p/>
* This value may be overridden by defining the system property
* {@code jdk.map.althashing.threshold}. A property value of {@code 1}
* forces alternative hashing to be used at all times whereas
* {@code -1} value ensures that alternative hashing is never used.
*/
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
构造方法
//这个构造方法是传入初始容量和扩容因子
//当需要扩容的时候,根据扩容因子计算进行扩容
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//当初始容量太大,大于了允许的最大值时,使用最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//判断加载因子必须是大于0,而且必须是数字
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//初始化时将一个Map集合放入新创建的HashMap
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
inflateTable(threshold);
putAllForCreate(m);
}
put方法
JDK 的底层源码很多都用到了位运算,先回顾下:
左移<<:高位舍掉,低位补0
右移>>:低位舍掉,高位补0;
public V put(K key, V value) {
//在实例化HashMap的时候,只是将初始化容量值赋值了。
//但是没有初始化数组,所以第一次进来的话,
//table==EMPTY_TABLE 肯定是成立的。而且会初始化数组
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
//这里是得到通过key计算出来的hash值,这个hash值通过
//位移运算和hashseed进行位运算得到
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//和快速失败有关
modCount++;
addEntry(hash, key, value, i);
return null;
}
private void inflateTable(int toSize) {
// Find a power of 2 >= toSize
//官方的解释:找到一个大于等于toSize的2的幂次方
int capacity = roundUpToPowerOf2(toSize);
//记录下一次扩容的阈值大小,
//这边计算是通过本次初始的容量* 扩容因子
//得到的值作为下一次扩容的阈值大小
//当添加元素的数组大小大于等于阈值了,就需要进行扩容
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
int capacity = roundUpToPowerOf2(toSize)
官方的解释:找到一个 2 的幂次方数,要求大于等于toSize(初始化的容量大小)。比如传入的容量是 12,那么最终初始化的容量就是 16;如果是 15,也是 16;如果是 17,则就是 32,反正就是大于等于该数的 2 的幂次方的最小值。
return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
这行代码的使用了两个三目运算符。第一个运算符比较简单,就是要设置的数组大小是否大于了允许的最大容量,如果大于则使用最大的容量,否则又一个三目运算符。这个是重点,假如传入的是 11:
(num -1) << 1
等价于“10 << 1”也就是 10 左移一位,表示为
10 的二进制:0000 1010
10 << 1:0001 0100 也就是 20
然后看Integer.highestOneBit:
public static int highestOneBit(int i) {
// HD, Figure 3-1
i |= (i >> 1);
i |= (i >> 2);
i |= (i >> 4);
i |= (i >> 8);
i |= (i >> 16);
return i - (i >>> 1);
}
传入的是 20,二进制是:0001 0100。执行i |= (i >> 1);
i: 0001 0100
i >> 1:0000 1010
|: 0001 1110
得到结果:0001 1110。执行i |= (i >> 2);
i: 0001 1110
i >> 2:0000 0111
|: 0001 1111
得到的结果:0001 1111。执行i |= (i >> 4);
i: 0001 1111
i >> 4:0000 0001
|: 0001 1111
得到的结果:0001 1111。执行i |= (i >> 8);
i: 0001 1111
i >> 8:0000 0000
|: 0001 1111
得到的结果: 0001 1111。执行i |= (i >> 16);
i: 0001 1111
i >> 16:0000 0000
|: 0001 1111
得到的结果: 0001 1111。执行return i - (i >>> 1);
i: 0001 1111
i>>>1: 0000 1111
i-(i >>> 1):0001 0000
最终的结果:0001 0000,十进制就是 16 。传入的初始容量是 11,实际初始化的容量是 16,也就验证了Find a power of 2 >= toSize
,找到一个 2 的幂次方数并且大于等于 toSize,toSize 就是传入的 11。
这个操作的规律就是最终的结果不是 4 个 0 就是 4 个 1,它将低位的 1 尽量的移动到高位去,不管移动多少位,反正取 ”与“ 后的结果肯定还是 ”与“ 之前的结果。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
这个方法就是 hashmap 计算 key 的 hash 值方法,比较复杂,大体意思就是将 key 的 hash 值计算出来然后做位运算,为什么要右移这么多,核心思想就是避免出现太多了 hash 冲突。因为取余都是操作低位,hash 碰撞的概率会提高,为了减少 hash 碰撞,右移就可以将高位也参与运算,减少了 hash 碰撞。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
上面方法当 key==null 的时候调用,HashMap 允许传入一个 null 作为 key。如果是 null 的 key,默认放在第一位,也就是数组为 0 的位置,当放置 null key 的时候,第 0 个位置已经被占用了,这个时候就会存在第 0 个位置的链表上。
上面代码可以看出 for 循环中是从数组的第一个位置开始循环的,也就是说 key = null 的数据是放在数组为 0 的位置上或者数组为 0 的链表上。上面方法是要返回一个值的,如果添加 key = null 的数据的时候,这个 null key 已经有了,那么会替换这个新的值,然后返回之前的值。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容的大小为原来数组长度的2倍,比如当前长度16,扩容
//后就是32(注意是 table 长度,而不是 threshold)
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//创建一个Entry存放添加的元素
createEntry(hash, key, value, bucketIndex);
}
上面方法就是添加元素,也就是 put 方法的核心逻辑处理,首先判断当前的 map 的 size 是否大于了这个阈值,这个阈值初始化长度为 16(构造 hashmap 为空的情况下),当第一次 put 的时候会计算数组的初始长度然后这个阈值(=16 * 扩容因子);所以当 size 大于等于阈值过后,并且要添加的这个鼠标下标位置已经有值了就开始扩容;
扩容
数据量很大的情况下,扩容将会带来性能的损失。在性能要求很高的地方,这种损失很可能很致命。
//这个newCapacity默认是扩容前的2倍,
void resize(int newCapacity) {
//首先声明一个Entry数组用来存放原来的数组信息
Entry[] oldTable = table;
//得到原来的数组长度,然后判断扩容的大小是否已经达到了最大的长度,
//如果大于了数组的最大长度,那么就设置阈值为最大数组的长度,则下次无法再扩容了
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//声明新的数组信息
Entry[] newTable = new Entry[newCapacity];
//数据的元素转移,就是讲oldTable转移到newTable中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
//这个方法是干什么的呢?
//简单来说就是currentAltHashing 和useAltHashing异或如果为true
//那么hashSeed就会重新赋值,那么什么时候为true呢?
//比如说第一次初始化的时候hashSeed肯定是0,所以currentAltHashing false
//而useAltHashing 主要是由Holder.ALTERNATIVE_HASHING_THRESHOLD来控制的
//根据下图来看,主要由jdk.map.althashing.threshold这个参数控制
//这个参数可以在运行参数中设置 -Djdk.map.althashing.threshold=3,比如为3
//那么useAltHashing 就很有可能为true,那么switching就会ture,那么
//hashseed就会重新计算值,就不是0了,那么这个值到底有什么用呢?
//这个值的用途在transfer方法中,其实它的用法其实就是为了使hash算法
//变得更复杂一些,也就是更散列一些,其实就是这个作用,更散列一些,计算出来
//的下标也就更好一些
final boolean initHashSeedAsNeeded(int capacity) {
boolean currentAltHashing = hashSeed != 0;
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}
//扩容的核心方法,就是讲原来的数组复制到新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//这里使用了2层循环来进行数组的复制,为什么要使用2层循环呢?
//因为hashmap是一般的数组结构,在数组元素上的单向链表结构,所以如果发生了数组
//的扩容,需要两层循环来操作
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
//hashSedd的判断
if (rehash) {
//使hash更散列一些
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//通过hash计算出来的下标,
//在新的数组中赋值给原来数组的next
//其实就是新的数组下标引用了原来的下标数据的引用地址
e.next = newTable[i];
//将本次元素与原来解除关系过后,将引用变成原有的地址
//具体看图
newTable[i] = e;
e = next;
}
}
}
扩容数组移动图
上图就是transfer方法的2层循环执行的过程
CreateEntry
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
如果有需要扩容,扩容完成过后就进行最后一步添加Entry元素,看第二行代码,第二行代码是
Entry<K,V> e = table[bucketIndex]
,这个e可能为空,也可能不为空,当e为空的时候,那么证明这个下标上还没有元素被添加,那么bucketIndex位置上直接存放添加的元素即可;如果不为空,则证明bucketIndex上已经存在元素了,那么这个时候就要添加到bucketIndex的链表上,因为hashmap的链表默认是单向链表,Hashmap的单向链表默认是头插法,根据扩容的流程图就可以知道,所以当e不为空的时候,那么这个元素默认在头部,其他元素往下移,也就有了 table[bucketIndex] = new Entry<>(hash, key, value, e)这句代码的含义。
Entry的基本结构
就根据createEntry方法来了解下Entry的数据结构,大概了解一下即可
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
这是HashMap在put的时候最后调用createEntry的方法,这个方法有4个参数
int h:可以对应的hash值;
K k:key的明文值;
V v:put的值;
get获取元素
get获取元素比较简单,就算没有看过源码,也大概能够猜出来,不就是根据key循环HashMap的Entry数组嘛,找到key相等的就返回,这是我初探HashMap源码时,带着这种思维去看的,所以看源码先要理解它的原理,带着原理快速过一遍源码,刚开始不要纠结细节,否则你看源码的兴趣可能在半小时左右,你专到一个方法细节里面出不来,半小时过后可能就失去兴趣了,所以要根据自己所猜的原理去快速过一遍,然后后面再慢慢理细节。
public V get(Object key) {
//如果Key为空,那么就找到Entry数组中key==null的一个元素Entry
//在HashMap中是允许Null key和Null value的
if (key == null)
return getForNullKey();
//循环Entry数组,根据找到对应的value
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
//这个方法比较简单,就是在table中找到一个null key的Entry,然后返回
private V getForNullKey() {
if (size == 0) {
return null;
}
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
//根据key找到Entry,这里的循环很简单,就是根据传进来的的key
//计算出来一个hash,然后通过IndexFor找到指定的位置
//然后在这个位置上循环单向链表,找到对应的key,然后返回这个
//key对应的Entry对象
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
删除元素remove
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
//根据key删除元素和get差不不大,都是新通过key计算hash
//然后再通过indexFor得到要删除的元素所在的位置
//然后循环,找到要删除元素的key,这里删除使用的是链表出队的方式
final Entry<K,V> removeEntryForKey(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
//链表的指向直接指向下一个元素 ,那个本元素失去引用,也就是
//简单粗暴的出队方式
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
在remove或者put的时候都有代码,modCount++和size++ / size–
size其实很好理解,就是Hashmap的真实元素个数,而不是数组长度,这个元素个数在一定程度上可能大于数组长度,因为有链表结构;
而这个modCount是什么呢?这个涉及到一个异常,修改异常ConcurrentModificationException,也就是fast-fail(快速失败)是HashMap的一种自我保护模式,就是说在迭代的时候是不循环对map进行添加或者删除的,所以个modCount表示的是对map的操作次数,只是改变数组元素时候会记录这个modCount,所以在迭代的时候声明一个期望值=modCount,然后每次迭代判断这个期望值是否还是等于modCout,如果不等于,则抛出异常,是一种快速失败的模式。
Hashmap的其他的api就不做记录了,其实都差不多,都是围绕Entry这个来展开进行操作的,没有什么太多太难的地方,理解原理和思想即可。
四、JDK8 的 HashMap 源码分析
1️⃣HashMap的默认容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
2️⃣HashMap的最大支持容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
3️⃣HashMap的默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
4️⃣Bucket中链表长度大于该默认值,转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
5️⃣Bucket中红黑树存储的Node小于该默认值,转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
6️⃣桶中的Node被树化时最小的hash表容量。(当桶中Node的数量大到需要变红黑树时,若hash表容量小于MIN_TREEIFY_CAPACITY时,此时应执行resize扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是 TREEIFY_THRESHOLD的4倍)
static final int MIN_TREEIFY_CAPACITY = 64;
7️⃣table:存储元素的数组,总是 2 的 n 次幂。
8️⃣entrySet:存储具体元素的集
9️⃣size:HashMap中存储的键值对的数量
🔟modCount:HashMap扩容和结构改变的次数。
1️⃣1️⃣threshold:扩容的临界值,=容量*填充因子
1️⃣2️⃣loadFactor:填充因子
静态成员常量
public class HashMap {
//默认数组的初始值是16,必须为2的倍数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//数组的最大值。2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子,0.75,和数组大小一起使用
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
//默认从红黑树转为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//默认转换为红黑树后最小的红黑树容量
static final int MIN_TREEIFY_CAPACITY = 64;
}
成员变量
public class HashMap<K, V> {
//一开分析的 `储存数据的数组`
transient java.util.HashMap.Node<K,V>[] table;
//用于entrySet()和values(),返回一个迭代器遍历Map结构
transient Set<Map.Entry<K,V>> entrySet;
//整个hashmap 所包含的节点数
transient int size;
//hashmap 的结构修改次数,比如 Put,remove的次数,
//和上面的 迭代器配合使用,在迭代过程中,如果其它线程更改了这个值,
//则会抛出 `ConcurrentModificationException`异常
transient int modCount;
//hashmap扩容的阈值,值为 loadFactor*table.length e.g: 0.75 * 16 = 12
//也就是说默认是当数组大小超过 12时就会进行数组扩容
int threshold;
//加载因子,默认值上图已经说明
final float loadFactor;
}
Node类的成员变量
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
key、key 的 hash 值、key 对应的 value 和下一个节点的引用,其中链表的形成就是 next 这个引用的作用。
put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
很清楚,通过 hash(key) 获取到了 key 的 hash 值,然后调用了 putVal():
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
这是 putVal 的原始方法,看起来有点复杂,每行注释如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//声明本地变量 tab,p,n,i(提高性能,effective java)
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
//将成员变量 table 赋值给本地变量 tab,并且将tab的长度赋值给本地变量 n
tab = table;
if (tab != null) {
n = tab.length;
}
/* 如果tab为空或者 数组长度为0,进行初始化,调用 resize()方法,并且获取赋值后的数组长度 */
if (tab == null || n = 0) {
tab = resize();
n = tab.length;
}
/* 根据key的hash值得到当前key在数组中的 位置,赋值给 i */
i = (n - 1) & hash;
/* 将i在数组中对应的key值去除赋值给p,所以p代表当前的key */
p = tab[i];
/* 判断当前数组中取出来的key是否为空(数组中没有),就new一个新的节点,并且放在这个索引 i的位置 */
if (p == null) {
tab[i] = newNode(hash, key, value, null);
/* 如果不为空,那就表示已经有这样的hash 值已经存在了,可能存在hash冲突 或者 直接替换原来的value */
} else {
/* 声明本地变量 e, k */
Node<K, V> e;
K k;
/* 如果取出来的节点 hash值相等,key也和原来的一样( == 或者 equals方法为true),直接将 这个节点
* p 赋值给刚刚声明的本地变量 e (这个操作很重要,在心中记住)
* 另外这里还将 节点 p的key 赋值给了本地变量 k
* */
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
/* 如果 hash值一样,但不是同一个 key,则表示hash冲突,接着判断这个节点是不是 红黑树的节点
* 如果是,则生成一个红黑树的节点然后赋值给本地变量 e */
} else if (p instanceof TreeNode) {
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
/* 不是红黑树,hash冲突了,这个时候开始扩展链表 */
} else {
/* 声明一个本地变量 binCount,开始遍历 p节点后面的链表 */
for (int binCount = 0; ; ++binCount) {
/* 首先将p节点的 next(链表的下一个)赋值给 本地变量e */
e = p.next;
/* 如果e为空,表示p指向的下一个节点不存在,这个时候直接将 新的 key,value放在链表的最末端 */
if (e == null) {
p.next = newNode(hash, key, value, null);
/* 放入后,还要判断下 这个链表的长度是否已经大于等于红黑树的阈值 (前面分析静态成员变量已经说明),
* 一旦大于,就可以变形,调用 treeifyBin方法将原来的链表转化为红黑树 !
* */
if (binCount >= TREEIFY_THRESHOLD - 1) { // -1 for 1st
treeifyBin(tab, hash);
}
break;
}
/* 如果不为空,表示还没有到链表的末端,
将 e 赋值给 p(p的下一个节点赋值给p),开启下一次循环 */
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
/* e不等于null,则表示 key值相等,替换原来的value即可,
* 这里需要注意,这里不是表示 hash冲突(再观察下前面的分析),
* hash冲突链表的扩展已经在最后一个 else完成了!
* */
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
/* 替换新值后,回调该方法(子类可扩展) */
afterNodeAccess(e);
/* 返回原来的 key对应的旧值 */
return oldValue;
}
}
/* 完成一次 put方法后,加一次 modCount,看前面成员变量分析 */
++modCount;
/* 加入了新节点,把 size 自加,并且 判断是否已经大于要扩容的阈值(观察前面成员变量分析),开始扩容 */
if (++size > threshold)
resize();
/* 插入新节点后,回调方法(子类可扩展) */
afterNodeInsertion(evict);
/* 插入的新节点,直接返回 null即可 */
return null;
}
其中所有代码均已经加上详细注释,这里值得注意的是,由于这个方法没有任何 线程同步手段,所以不论是在查找对应的key,还是扩容,插入节点,增加size,modCount等,肯定会出现问题(这里先预留一篇文章,ConCurrentHashMap源码分析),所以多线程环境下,绝对不能使用HashMap,
而应该使用ConCurrentHashMap。
当然到了现在,那个面试题的答案也已经能够较为完整的回答出来了!
- 上面较为详细的分析了put方法后,注意到 resize() 在这个方法中起到了关键作用,初始化,以及扩容。那接着来观察下 resize():
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这个方法看起来也较为复杂,同样作下简单分析:
final Node<K, V>[] resize() {
/* 同样声明本地变量,得到原来的数组,提高性能 */
Node<K, V>[] oldTab = table;
/* 获得数组的长度 */
int oldCap = (oldTab == null) ? 0 : oldTab.length;
/* 获取扩容阈值 */
int oldThr = threshold;
/* 新的数组长度,新的阈值 */
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
/* 将原来的数组长度 * 2 判断是否小于最大值,并且原来的数组长度大于 默认初始长度(16)
* 直接双倍扩容, 阈值,长度都 * 2
* */
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
/* 第一次调用 resize方法,初始化数组长度,阈值,这里就对应前面成员变量的分析了:
* 阈值 = 加载因子 * 数组长度
* */
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
/* 根据前面计算出来的新长度,声明一个新数组 */
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
/* 开始将旧数组的长度复制到新数组 */
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
if ((e = oldTab[j]) != null) {
/* 原数组的值先置换为null,帮助gc */
oldTab[j] = null;
/* 如果节点的next不为空(没有形成链表),直接复制到新数组 */
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
/* 不为空但是已经是 红黑树了,按红黑树规则置换 */
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
/* 已经形成链表了,循环将链表的引用到新数组,不再使用链表 */
else { // preserve order
/* 声明四个引用,可以防止多线程环境下 死循环! */
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
/* 最后返回新数组 */
return newTab;
}
注释已经简要说明流程,这里可以看出有数组复制以及重新计算hash的操作,所以在实际开发中使用 HashMap 的时候,最好设置一个初始容量,防止经常扩容操作耗费性能!
- 好了,HashMap 两个关键方法都分析完毕了,接下来分析 get(key):
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
get 方法首先通过 hash(key) 获取到了 hash 值,接着通过 getNode(hash) 获取节点,所以重点看下 getNode 方法:
final Node<K, V> getNode(int hash, Object key) {
/* 声明本地变量,提高性能 */
Node<K, V>[] tab;
Node<K, V> first, e;
int n;
K k;
/* 本地变量赋值,n为数组长度 */
tab = table;
if (tab != null) {
n = tab.length;
}
/* 通过 hash值算出key在数组中的 位置,取出该节点 */
first = tab[n - 1] & hash;
/* 不为空,表示key在数组中存在,接下来开始遍历链表获取红黑树,找出具体位置 */
if (tab != null && n > 0 && first != null) {
/* 如果链表或者红黑树的第一个节点 hash值,key相等,这个节点就是要找的,直接返回 */
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
/* 开始遍历链表 */
if ((e = first.next) != null) {
/* 如果是红黑树,直接按树规则 查找然后返回 */
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
do {
/* 遍历链表找到了,返回 */
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
/* 最后没有找到,直接返回null */
return null;
}
值得注意的是, 在链表中查找节点采用的是遍历的方式,所以一旦链表过长,查找性能就较慢,这也是为什么 jdk1.8 会在链表长度超过阈值的时候将链表转换为红黑树的原因!链表时间复杂度为O(n),红黑树为 O(logn)。
五、JDK8 中 HashMap 底层链表转红黑树的阈值为什么是 8?红黑树转链表为什么是 6?
和 hashcode 碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡。
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
1️⃣链表转换为红黑树
红黑树中的 TreeNode 是链表中的 Node 所占空间的 2 倍,虽然红黑树的查找效率为 O(logN),要优于链表的 O(N),但是当链表长度比较小的时候,即使全部遍历,时间复杂度也不会太高。所以要寻找一种时间和空间的平衡,即在链表长度达到一个阈值之后再转换为红黑树。链表转换为红黑树的最终目的,是为了解决在 map 中元素过多,hash 冲突较大,而导致的读写效率降低的问题。在源码的 putVal 方法中,有关红黑树结构化的分支为:
//此处遍历链表
for (int binCount = 0; ; ++binCount) {
//遍历到链表最后一个节点
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表元素个数大于等于TREEIFY_THRESHOLD
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//红黑树转换逻辑
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
即当链表元素个数大于 8 的时候,就会转换为红黑树。之所以是 8,是因为 Java 的源码贡献者在进行大量实验发现,hash 碰撞发生 8 次的概率非常低,几乎为不可能事件。如果真的发生了 8 次碰撞,说明由于元素本身和 hash 函数的原因,此时的链表性能已经很差了,操作的 hash 碰撞的可能性非常大,后序可能还会继续发生碰撞。所以,在这种极端的情况下才会把链表转换为红黑树,链表转换为红黑树也是需要消耗性能的,大部分情况下 hashMap 还是使用链表。treeifyBin() 源码:
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//首先tab的长度是否小于64,
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
//小于64则进行扩容
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
//否则才将列表转换为红黑树
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
可以看到在 treeifyBin 中并不是简单地将链表转换为红黑树,而是先判断 table 的长度是否大于 64。如果小于 64,就通过扩容的方式来解决,避免红黑树结构化。链表长度大于 8 有两种情况:
- table 长度足够,hash 冲突过多
- hash 没有冲突,但是在计算 table 下标的时候,由于 table 长度太小,导致很多 hash 不一致的
第二种情况是可以用扩容的方式来避免的,扩容后链表长度变短,读写效率自然提高。另外,扩容相对于转换为红黑树的好处在于可以保证数据结构更简单。由此可见并不是链表长度超过 8 就一定会转换成红黑树,而是先尝试扩容。
2️⃣红黑树转换为链表
基本思想是当红黑树中的元素减少并小于一定数量时,会切换回链表。而元素减少有两种情况:
- 调用 map 的 remove 方法删除元素
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//根据hash值以及key判断当前的是否相等,如果相等直接返回
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
//判断是否为红黑树结构
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
//如果不是则为链表结构
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//判断当前桶是否是红黑树结构,如果是的话
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null)
tab[index] = first = succ;
else
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null)
return;
if (root.parent != null)
root = root.root();
//判断是否解除红黑树结构
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; // swap colors
TreeNode<K,V> sr = s.right;
TreeNode<K,V> pp = p.parent;
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
root = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
}
else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // detach
TreeNode<K,V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
moveRootToFront(tab, r);
}
可以看到,此处并非当节点数小于 UNTREEIFY_THRESHOLD 时才转换,而是通过红黑树根节点及其子节点是否为空来判断。
- resize 的时候,对红黑树进行了拆分
resize 的时候,判断节点类型,如果是链表,则将链表拆分,如果是 TreeNode,则执行 TreeNode 的 split 方法分割红黑树,而 split 方法中将红黑树转换为链表的分支如下:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
//在这之前的逻辑是将红黑树每个节点的hash和一个bit进行&运算,
//根据运算结果将树划分为两棵红黑树,lc表示其中一棵树的节点数
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
这里才用到了 UNTREEIFY_THRESHOLD 的判断,当红黑树节点元素小于等于 6 时,才调用 untreeify 方法转换回链表。红黑树转链表的阈值为 6,主要是因为,如果也将该阈值设置为 8,那么当 hash 碰撞在 8 时,会发生链表和红黑树的不停相互激荡转换,白白浪费资源。中间有个差值 7 可以防止链表和树之间的频繁转换。
3️⃣总结
如果设计成链表个数超过 8 则链表转换成树结构,链表个数小于 8 则树结构转换成链表,HashMap 不停的插入/删除元素,链表个数在 8 左右徘徊,就会频繁的发生红黑树转链表,链表转红黑树,效率会很低下。
- hashMap 并不是在链表元素个数大于 8 就一定会转换为红黑树,而是先考虑扩容,扩容达到默认限制后才转换。
- hashMap 的红黑树不一定小于 6 的时候才会转换为链表,而是只有在 resize 的时候才会根据 UNTREEIFY_THRESHOLD 进行转换。
六、HashMap 和 HashTable 的区别
- HashMap 是线程不安全的,HashTable 是线程安全的。
- 由于线程安全,所以 HashTable 的效率比不上 HashMap。
- HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null,而 HashTable不允许。
- HashMap 默认初始化数组的大小为 16,扩容时扩大两倍。HashTable 为 11,扩容时扩大两倍 +1。
- HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode。
七、HashMap & TreeMap & LinkedHashMap 使用场景?
一般情况下,使用最多的是 HashMap。
- HashMap:在 Map 中插入、删除和定位元素时;
- TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;
- LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。
八、只有 HashMap 可操作 null
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class mapDemo {
public static void main(String[] args) {
HashMap map = new HashMap();
map.put(null, null);
map.get(null);
map.remove(null);
Map table = new Hashtable();
// table.put(null, "value");//NPE
// table.put("key",null);//NPE
// table.get(null);//NPE
// table.remove(null);//NPE
ConcurrentHashMap cm = new ConcurrentHashMap();
// cm.put(null, "value");//NPE
// cm.put("key", null);//NPE
// cm.get(null);//NPE
// cm.remove(null);//NPE
}
}