一 存储结构
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
}
transient Node<K,V>[] table;
内部存储的单元如上所示,整体上就是数组加链表的桶状结构。
二 put操作
put(key,value)内部调用的是putVal() 下面是源码 jdk1.8采用的是尾插法
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 { //查找节点(键值为key) 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; } }
//以下代码好像是为了linkedhashmap服务的 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; }
代码除了红黑树的部分引出两个部分:1. 为什么采取hash & 长度-1 的方式找数组位置 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; }
对于构造函数中传入的整数,进行如下操作。结论就是得到的数,是大于等于传入值的最小 2 的幂。也就是传入64 就是64 ,传入127 就是 128。
这算法想起来不好想,但看代码还是好理解。对于65这种,减去1,就是 100 0000 第一次运算 是110 0000 第二次: 111 1000,结果变成1111111(全1) 最后加上1 必然是2 的整次幂。
这也解决了 hash运算为什么用 hash & n-1,因为&的效率大于除法和取余运算,而且由于是2的整次幂,按位与和取余操作效果一样,但是速度更快。
此外 node节点中的hash 值 ,是这样得到的,也就是原来key hashcode高16位和hashcode亦或。为的是增加高16位的参与感,减少hash冲突,如果key的hashcode 是 0A01 0B01 0C01 如果直接进行 按位与操作,那么他们都被映射到同一个位置,而经过这么一轮操作会,会减少hash碰撞的情况。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
而扩容主要是resize方法 主要是这段代码 ,新的数组是旧的数组的二倍,如下是旧的数据迁移到新数组中
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 { //分成两类 比如旧容量是16 对于 hash 17 1 他们都在下标为1的位置,但是 &16 ,要么是0 要么是1 。这就是分类的标准 为0 的放在新的数组的1的位置,为1的放到17 的位置 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; } } }
四 总结
1 .hash值的算法,键的hash值与 高16亦或,保留高位信息
---映射下标值 &(容量-1)得到实际的
2. 保证容量是2的幂 采取按位或
3. .hashMap的结构
在1.8做出的改变,数组加红黑树 链表大于8就会变树,
变树的代价比较高,所以还增加了一个条件,就是数组大小要大于64,否则采取扩容的方法,解决链表过长的问题。
4.hashmap可以插入空值键
有单独的方法,永远插在第一个链表,也就是数组下标为0的位置
5.遍历方法
map.entrySet() 这是拿到了所有的键值对
keyset() 这个是拿到了所有的键值
6.并发下的问题
put()丢失更新
resize()也可能丢失更新