HashMap源码分析
HashMap的历史
HashMap最早是在jdk1.2中开始出现的,一直到jdk1.7一直没有太大的变化。
但是到了jdk1.8突然进行了一个很大的改动。其中一个最显著的改动就是:
之前jdk1.7的存储结构是数组+链表,到了jdk1.8变成了数组+链表+红黑树。
另外,HashMap是非线程安全的,进行增删改操作的时候,是不能保证数据的一致性的。
红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。
HashMap名词介绍
名称 | 用途 |
---|---|
initialCapacity | HashMap 初始容量 |
loadFactor | 负载因子 |
threshold | 当前 HashMap 所能容纳键值对数量的最大值,超过这个值,则需扩容 |
负载因子(loadFactor)
对于 HashMap 来说,负载因子是一个很重要的参数,该参数反应了 HashMap 桶数组的使用情况。
通过调节负载因子,可使 HashMap 时间和空间复杂度上有不同的表现。
当我们调低负载因子时,HashMap 所能容纳的键值对数量变少。
扩容时,重新将键值对存储新的桶数组里,键的键之间产生的碰撞会下降,链表长度变短。此时,HashMap 的增删改查等操作的效率将会变高,这里是典型的拿空间换时间。
相反,如果增加负载因子(负载因子可以大于1),HashMap 所能容纳的键值对数量变多,空间利用率高,但碰撞率也高。这意味着链表长度变长,效率也随之降低,这种情况是拿时间换空间。
至于负载因子怎么调节,这个看使用场景了。一般情况下,我们用默认值就可以了。
Node[] table
即哈希桶数组 ,Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。
Node[] table的初始化长度length(默认值是16),
Load factor为负载因子(默认值是0.75),
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。
threshold = length * Load factor。
也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false; }
}
源码解析
初始化 threshold 的方法
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
总结起来就一句话:找到大于或等于 cap 的最小2的幂。
put(K key, V value)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
也就是说,put方法其实调用的是putVal方法。putVal方法有5个参数:
(1)第一个参数hash:调用了hash方法计算hash值
(2)第二个参数key:就是我们传入的key值,也就是例子中的张三
(3)第三个参数value:就是我们传入的value值,也就是例子中的20
(4)第四个参数onlyIfAbsent:也就是当键相同时,不修改已存在的值
(5)第五个参数evict :如果为false,那么数组就处于创建模式中,所以一般为true。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //定义的数组
Node<K,V> p; //当前插入的节点
int n, i;
//第一部分
//如果table尚未初始化,则此处进行初始化数组,并赋值初始容量,重新计算阈值
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//第二部分
//判断hash值,不冲突,new一个Node插入进去
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//第三部分
//处理冲突,如果通过hash找到的位置有数据,发生碰撞
else {
Node<K,V> e;
K k;
//第三部分--A
//p是当前节点,如果需要插入的key和当前hash值指定下标的key一样,一样的直接替换旧值e
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//第三部分--B
//如果此时桶中数据类型为 treeNode,使用红黑树进行插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//第三部分--C
//此时桶中数据类型为链表
else {
//看这里的 .next,如果是数组,就会进去遍历
//没有 就newNode //化树的阈值,TREEIFY_THRESHOLD,所以不存在并且在链表末尾插入元素的时候
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;
}
//如果链表中有新插入的节点位置数据不为空,则此时e 赋值为节点的值,跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//经过上面的循环后,如果e不为空,则说明上面插入的值已经存在于当前的hashMap中,
//那么更新指定位置的键值对
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;
}
代码看,put方法分为三种情况:
- table尚未初始化,对数据进行初始化
- table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位置
- table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作:
- 判断插入的key如果等于当前位置的key的话,将 e 指向该键值对
- 如果此时桶中数据类型为 treeNode,使用红黑树进行插入
- 如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)
扩容前的小知识
怎么计算Hash值?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
与16异或的位运算。
在数据结构中,我们处理hash冲突常使用的方法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而hashMap中处理hash冲突的方法就是链地址法。
“链”,很容易想到,因为常用删除修改等操作,所以效率更高。
initialCapacity初始容量(你输入的最接近2的N次幂的数字),loadFactor负载因子(0.75,泊松分布)。
/* ---------------- Public operations -------------- */
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
扩容 resize()
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
//初始化或加倍表大小。如果为空,则根据字段阈值中持有的初始容量目标进行分配。否则,因为我们使用的是 2 的幂扩展,所以每个 bin 中的元素必须保持相同的索引,或者在新表中以 2 的幂的偏移量移动。
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; //threshold 临界点
int newCap, newThr = 0;
//第一部分,扩容
//如果超过了直接将阈值设置为整数的最大值,没有,则,通过移位操作扩大2倍。
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;
//threshold 和 table 皆未初始化情况,此处即为首次进行初始化
//也就在此处解释了构造方法中没有对threshold 和 初始容量进行赋值的问题
else { // zero initial threshold signifies using defaults
//如果阈值为零,表示使用默认的初始化值
//这种情况在调用无参构造的时候会出现,此时使用默认的容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
//此处阈值即为 threshold=initialCapacity*loadFactor
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// newThr 为 0 时,按阈值计算公式进行计算,容量*负载因子
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;
//第三部分
//将旧的数据迁移到新的数组
//如果之前的数组桶里面已经存在数据,由于table容量发生变化,hash值也会发生变化,需要重新计算下标
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)
//直接将数据存放到新计算的hash值下标下
newTab[e.hash & (newCap - 1)] = e;
//如果是TreeNode数据结构
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//对于链表,数据结构
else { // preserve order
//如果是链表,重新计算hash值,根据新的下标重新分组
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;
}
resize分为以下几步:
- 首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍
- 扩容后创建新的table,并对所有的数据进行遍历
- 如果新计算的位置数据为空,则直接插入
- 如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组
- 如果是红黑树,则需要进行拆分操作
get方法,查找
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab;
Node<K,V> first, e;
int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != 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);
}
}
return null;
}
hash源码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这段代码叫做扰动函数,也是hashMap中的hash运算,主要分为下面几步:
- key.hashCode(),获取key的hashCode值,如果不进行重写的话返回的是根据内存地址得到的一个int值
- key.hashCode() 获取到的hashcode无符号右移16位并和元hashCode进行^ ,这样做的目的是为了让高位与低进行混合,让两者都参与运算,以便让hash值分布更加均匀
取模运算 (n - 1) & hash
在hashMap的代码中,在很多地方都会看到类似的代码:
first = tab[(n - 1) & hash])
hash算法中,为了使元素分布的更加均匀,很多都会使用取模运算,
在hashMap中并没有使用hash%n这样进行取模运算,而是使用(n - 1) & hash进行代替,
原因是在计算机中,&的效率要远高于%。
需要注意的是,
只有容量为2的n次幂的时候,(n - 1) & hash 才能等效hash%n,
这也是hashMap 初始化初始容量时,无论传入任何值,都会通过tableSizeFor(int cap) 方法转化成2的n次幂的原因,
这种巧妙的设计真的很令人惊叹。
有兴趣的可以看一下一位大佬写的文章:由HashMap哈希算法引出的求余%和与运算&转换问题
remove方法,删除
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
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;
//根据key和key的hash值,查找到对应的元素
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;
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);
}
}
//如果查找的了元素node,移除即可
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
//如果是TreeNode,通过树进行移除
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//如果是第一个节点,移除第一个节点,将index下标的位置指向第二个节点
else if (node == p)
tab[index] = node.next;
else
//如果不是链表的第一个节点,则移除该节点
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
链表树化
static final int TREEIFY_THRESHOLD = 8;
/**
* 当桶数组容量小于该值时,优先进行扩容,而不是树化
*/
static final int MIN_TREEIFY_CAPACITY = 64;
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
/**
* 将普通节点链表转换成树形节点链表
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
// 桶数组容量小于 MIN_TREEIFY_CAPACITY,优先进行扩容而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd 为头节点(head),tl 为尾节点(tail)
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);
}
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
在扩容过程中,树化要满足两个条件:
- 链表长度大于等于 TREEIFY_THRESHOLD
- 桶数组容量大于等于 MIN_TREEIFY_CAPACITY
红黑树链化与拆分
链化
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
// 遍历 TreeNode 链表,并用 Node 替换
for (Node<K,V> q = this; q != null; q = q.next) {
// 替换节点类型
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
拆分
// 红黑树转链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
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;
/*
* 红黑树节点仍然保留了 next 引用,故仍可以按链表方式遍历红黑树。
* 下面的循环是对红黑树节点进行分组,与上面类似
*/
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;
}
}
if (loHead != null) {
// 如果 loHead 不为空,且链表长度小于等于 6,则将红黑树转成链表
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
/*
* hiHead == null 时,表明扩容后,
* 所有节点仍在原位置,树结构不变,无需重新树化
*/
if (hiHead != null)
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);
}
}
}
在将普通链表转成红黑树时,HashMap 通过两个额外的引用 next 和 prev 保留了原链表的节点顺序。
这样再对红黑树进行重新映射时,完全可以按照映射链表的方式进行。这样就避免了将红黑树转成链表后再进行映射,无形中提高了效率。
从源码上可以看得出,重新映射红黑树的逻辑和重新映射链表的逻辑基本一致。不同的地方在于,重新映射后,会将红黑树拆分成两条由 TreeNode 组成的链表。
如果链表长度小于 UNTREEIFY_THRESHOLD,则将链表转换成普通链表。否则根据条件重新将 TreeNode 链表树化。
被 transient 所修饰 table 变量
其实这个关键字的作用很好理解,
就是简单的一句话:将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会被序列化。
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
/**
* 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;
HashMap 并没有使用默认的序列化机制,而是通过实现readObject/writeObject
两个方法自定义了序列化的内容。
序列化 talbe 存在着两个问题:
- table 多数情况下是无法被存满的,序列化未使用的部分,浪费空间
- 同一个键值对在不同 JVM 下,所处的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能会发生错误。
第二个问题解释:HashMap 的get/put/remove
等方法第一步就是根据 hash 找到键所在的桶位置,
但如果键没有覆写 hashCode 方法,计算 hash 时最终调用 Object 中的 hashCode 方法。但 Object 中的 hashCode 方法是 native 型的,不同的 JVM 下,可能会有不同的实现,产生的 hash 可能也是不一样的。也就是说同一个键在不同平台下可能会产生不同的 hash,此时再对在同一个 table 继续操作,就会出现问题。
总结
- HashMap 底层数据结构在JDK1.7之前是由数组+链表组成的,1.8之后又加入了红黑树;链表长度小于8的时候,发生Hash冲突后会增加链表的长度,当链表长度大于8的时候,会先判读数组的容量,如果容量小于64会先扩容(原因是数组容量越小,越容易发生碰撞,因此当容量过小的时候,首先要考虑的是扩容),如果容量大于64,则会将链表转化成红黑树以提升效率。
- hashMap 的容量是2的n次幂,无论在初始化的时候传入的初始容量是多少,最终都会转化成2的n次幂,这样做的原因是为了在取模运算的时候可以使用&运算符,而不是%取余,可以极大的提上效率,同时也降低hash冲突。
- HashMap是非线程安全的,在多线程的操作下会存在异常情况(如形成闭环(1.7),1.8已修复闭环问题,但仍不安全),可以使用HashTable或者ConcurrentHashMap进行代替
HashMap这样的知识点还有很多,一篇文章是说不完的,至于更多的玄机需要我们一起去探索。
参考链接:
HashMap源码分析(jdk1.8,保证你能看懂) - 知乎 (zhihu.com)
Java 8系列之重新认识HashMap - 知乎 (zhihu.com)
有大量时间看这个,特别全: HashMap 源码详细分析(JDK1.8) - SegmentFault 思否