不难发现,上面第三个构造函数可以自定义加载因子和容量,首先判断传入的加载因子是否符合要求,然后根据制定的容量执行 tableSizeFor() 方法,它会根据容量来指定阈值,为何要多这一步呢?
因为buckets数组的大小约束对于整个HashMap都至关重要,为了防止传入一个不是2次幂的整数,必须要有所防范。tableSizeFor()函数会尝试修正一个整数,并转换为离该整数最近的2次幂。
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; }比如传入一个整数244,经过位移,或运算后会返回最近的2次幂 256
在集合中最常用的操作是存储数据,也就是插入元素的过程,在HashMap中,插入数据用的是 put() 方法。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }put方法没有做多余的操作,只是传入 key 和 value 还有 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; //哈希表如果为空,就做扩容操作 resize() if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //要插入位置没有元素,直接新建一个包含key的节点 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //如果要插入的桶已经有元素,替换 else { Node<K,V> e; K k; //key要插入的位置发生碰撞,让e指向p if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //没碰撞,但是p是属于红黑树的节点,执行putTreeVal()方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //p是链表节点,遍历链表,查找并替换 else { //遍历数组,如果链表长度达到8,转换成红黑树 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指向p if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 节点已存在,替换value,并返回旧value 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; }代码看上去有点复杂,参数有点乱,但理清逻辑后容易理解多了,源码大概的逻辑如下:
先调用 hash() 方法计算哈希值
然后调用 putVal() 方法中根据哈希值进行相关操作
如果当前 哈希表内容为空,做扩容
如果要插入的桶中没有元素,新建个节点并放进去
否则从要插入的桶中第一个元素开始查找(这里为什么是第一个元素,下面会讲到)
如果没有碰撞,赋值给e,结束查找
有碰撞,而且当前采用的还是 红黑树的节点,调用 putTreeVal() 进行插入
链表节点的话从传统的链表数组中查找、找到赋值给e,结束
如果链表长度达到8,转换成红黑树
最后检查是否需要扩容
put方法的代码中有几个关键的方法,分别是:
hash():哈希函数,计算key对应的位置
resize():扩容
putTreeVal():插入红黑树的节点
treeifyBin():树形化容器
前面两个是HashMap的桶链表操作的核心方法,后面的方法是Jdk1.8之后有关红黑树的操作,后面会讲到,先来看前两个方法。
哈希函数:hash()hash() 方法是HashMap 中的核心函数,在存储数据时,将key传入中进行运算,得出key的哈希值,通过这个哈希值运算才能获取key应该放置在 “桶” 的哪个位置,下面是方法的源码:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }从源码中可以看出,传入key之后,hash() 会获取key的hashCode进行无符号右移 16 位,然后进行按位异或,并把运算后的值返回,这个值就是key的哈希值。这样运算是为了减少碰撞冲突,因为大部分元素的hashCode在低位是相同的,不做处理的话很容易造成冲突。