继承关系
静态属性
private static final long serialVersionUID = 362498820763181265L; // 默认的容量 必须是2的次幂 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 最大容量, 容器的容量必须是 2的次幂且 <= 1 << 30 static final int MAXIMUM_CAPACITY = 1 << 30; // 构造器中没有指定时使用的默认负载因子 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;静态内部类
Node结点
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // 构造器 get set equals方法 ..... }TreeNode结点
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; // 操作树的方法 ...... }成员属性
// 表, 第一次使用时初始化 transient Node<K,V>[] table; // 保存缓存entrySet() transient Set<Map.Entry<K,V>> entrySet; // 包含的元素个数 transient int size; // fail - fast 机制用到的元素修改次数 transient int modCount; // 下一个要扩容的阈值(容量 * 负载因子) int threshold; // 哈希表的负载因子 final float loadFactor;设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 所以要想提升查找的效率, 需要让 M 即table尽可能的大, 但太大又会浪费太多空间。 所以HashMap采用动态扩容来根据N的值调整M。 使时间和空间维持一个良好的平衡。
与扩容有关的参数
参数含义capacitytable 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。size键值对数量。thresholdsize 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。threshold = newCapacity * loadFactorloadFactor负载因子,size 和 capacity的比例, 默认0.75。 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 如果哈希表还没初始化, 则oldcap = 0 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; } // oldCap << 1 扩容为原来的二倍, 且小于最大容量大于默认的容量 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) // 扩容阈值也提升两倍 newThr = oldThr << 1; // double threshold } // 原来的容量不大于0 且 扩容阈值大于0 则新容量等于oldThr扩容阈值, (调用带参构造器设置初始容量时, 会将调整好的初始化大小赋给threshold扩容阈值) else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // oldCap <= 0 也 oldThr <= 0 说明数组还未初始化 且没有设置初始容量, 则赋值默认容量 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 如果newThr还为0 则计算newThr if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 确定了 newThr, 进行赋值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 新建Node数组, 开始赋值rehash 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) { // 释放引用, 可以GC回收 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; // 如果 hash 值的 oldCap的最高位的二进制为0, 则接到lo链表里 if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } // 如果为1 则接到hi链表里 else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 遍历结束 lo链表接在新数组的原位置j if (loTail != null) { loTail.next = null; newTab[j] = loHead; } // hi链表接在新数组的 j + oldCap位置 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }JDK1.7 在进行resize() 时, 会将旧数组中链表的每个节点的hash依次重新计算并使用头插法插入新数组。 JDK1.8 使用与运算判断, 能够直接用尾插法创建好新数组对应节点上的2个链表,最后将链表在新数组中对应的位置插入。
在 put(), get() 方法和 resize() 中的部分代码中, 每当要遍历一个数组位置上上的所有元素时, 一般分为三步
判断 table 是否合法, 判断第一个节点是否符合条件。第一个节点条件不符合, 在接着判断该位置存储的是是红黑树还是链表。进行红黑树或链表的相关操作。存储:
JDK1.7 只使用单链表进行纵向存储。JDK1.8 单链表长度达到8时会转成红黑树。插入:
JDK1.7 采用头插法,因为只用单链表存储, 所以头插法可以提升插入的效率,但在并发环境下put()会形成环形链表,出现死循环问题,且resize()扩容后链表会逆序。JDK1.8 采用尾插法,且不会影响效率,因为加入了红黑树避免了单链表过长的情况,尾插法同时也解决了死循环和逆序的问题。扩容时索引的计算:
JDK1.7 在扩容时直接通过hash再次计算索引插入新数组。JDK1.8 在扩容时通过判断二进制位,可以直接计算出元素在新数组中的位置,并且可以一次性建立好链表,直接插入到新数组对应位置。