目录
一、概述
二、HashMap源码(JDK1.8)
1,构造函数:
1.1 无参的构造函数
1.2 指定初始容量的构造函数
1.3 可指定初始容量和加载因子的构造函数
2,put方法
3,get方法
4,对key为null的处理
三、HashMap1.7和1.8版本的对比
1,底层结构
2,数组的初始化
3,数组中的元素
4,对key为null值时候的处理
5,相同的地方
四、小结
JDK1.8版本的HashMap相对于1.7版本做了一些优化,比如说在扩容时候的数据转移改为了尾插法;在new出HashMap对象的时候,并没有对用于存放node的数组进行初始化,要在使用的时候才会初始化(有点懒加载的意思);对数组中的节点使用链表或红黑树类存储,防止链表过长时性能不好等。
只是给加载因子赋了一个默认值:0.75
没有初始化
// 只是给加载因子赋了一个默认值:0.75 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted }HashMap(int initialCapacity):
调用了两个参数的构造函数,所以重点去看两个参数的构造函数
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }HashMap(int initialCapacity, float loadFactor)
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); } /** * 返回一个大于等于给定目标容量的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; }传入了指定的容量,但是它把计算出的容量赋值给了阈值(threshold)
在源码中有一句注释:
// (The javadoc description is true upon serialization. // Additionally, if the table array has not been allocated, this // field holds the initial array capacity, or zero signifying // DEFAULT_INITIAL_CAPACITY.) int threshold;翻译一下就是:如果没有分配表数组,则该字段保存初始数组容量
意思就是先用阈值这个字段来记录一下容量
在上面有说过,1.8的HashMap要时put操作的时候才会初始化entry数组,当然了在这里该名字了:1.7定义为Entry,1.8定义为Node。
下面来看put方法。
直接调用了putVal方法,同时加了两个默认的参数如初putVal方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }putVal方法:
分那么几种情况:
情况一:第一次调用put方法,那么需要调用resize方法对Node[]进行初始化。情况二:根据key计算出数组下标,该下标下的node数据为空,则创建一个新的node进行插入即可情况三:根据key计算出数组下标,该下标下的node数据不为空,则判断需要判断是覆盖操作、还是插入操作。覆盖操作:直接覆盖新值返回旧值即可。
插入操作:插入的是链表还是红黑树,需要分别调用不同的插入方法来完成。
如果插入的是链表,则需要判断插入之后链表的长度,如果链表长度大于等于8,则需要把该链表转换为红黑树
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { // 定义Node数组、Node元素 Node<K,V>[] tab; Node<K,V> p; int n, i; // 如果table为null,则调用resize()方法 // 在第一次调用的时候,table是为null的,所以这个判断会进入,然后调用resize()方法来初始化table(即Node数组) if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash:计算出key应该放到哪个数组下表 // 如果该下表的元素为空,则创建一个新的Node节点,然后放到该下标下 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {// 如果该下标下的node不为空 Node<K,V> e; K k; // 判断是不是key相同的覆盖操作 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode)// 判断当前节点是不是红黑树 // 如果是红黑树则使用红黑树的put方法 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else {// 如果不是会头节点进行覆盖,也不是红黑树,那么就遍厉该node链表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) {// 如果遍厉到尾节点都没有找到相同的key,则进行插入 p.next = newNode(hash, key, value, null); // 插入完成后判断该链表的长度是否大于等于8(因为binCount是从0开始计数的,所以大于等于7时,链表的长度就大于等于8) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 如果链表长度大于等于8,则需要把链表转为红黑树 treeifyBin(tab, hash); break; } // 判断是不是key相同的覆盖操作 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 如果找到了已经存在的key,即需要覆盖 // 那么就替换value值,然后返回旧的值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;// 操作数+1 // 如果插入了一条数据之后需要扩容,那么就进行扩容 if (++size > threshold) resize(); afterNodeInsertion(evict);// 空方法 return null; }对Node数组进行初始化是在resize方法中完成的,下面来看一下resize方法:
情况一:第一次put操作时候,会对Node[]进行初始化情况二:如果是扩容,则创建一个新的Node[],并对数据进行转移转移分为链表数据转移和红黑树数据转移
链表数据转移的时候使用尾插法,每次插入都是在新数组元素链表的尾节点进行插入数据
final Node<K,V>[] resize() { // 将table数据赋值给局部变量oldTab Node<K,V>[] oldTab = table; // 第一次调用时table还没有初始化,则老的数组容量为0 int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 如果不是第一次调用,即oldTable数组的长度大于0,则需要进行扩容 if (oldCap > 0) { // 如果已经是最大容量了,就不进行扩容了 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 如果不是最大容量,则进行2倍扩容 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 第一次调用时,因为之前把容量放到了阈值字段,所以这里把阈值设置给初始容量 else if (oldThr > 0) newCap = oldThr; else { // 第一次调用时,oldCap为0,会进入此分支,给容量设置一个默认值16 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; // 第一次调用时,因为oldTab为null,所以不会进入此分支对数据进行转移,直接返回一个空的Node数组 // 不是第一次调用,而是进行扩容的时候,会进入这个if语句 if (oldTab != null) { // 遍厉老的Node数组 for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 如果该下标下只有一个node元素,则直接转移到新数组中 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 如果是红黑树,则调用红黑树节点的转移方法 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 遍厉该Node链表,对该链表进行数据转移 else { 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; }和1.7的HashMap差不多。
因为1.8多了个红黑树,所以get操作的查找就分为了链表的查找和红黑树的查找
public V get(Object key) { Node<K,V> e; // 根据key获取Node节点,如果节点不为null,则返回该节点的value值 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; // 两个条件 //1,Node数组不为空 //2,根据key的hash计算出的Node[]下标下的Node元素不为空 if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { // 首先查看一下所要get的元素是不是在头节点,如果是则直接返回该Node元素 if (first.hash == hash && ((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; }和1.7有点不同,1.7是单独写了个方法来处理key为null的情况,把数据放到数组下标为0的位置。
而1.8则是在计算hash的失火,如果key为null,则给hash值一个默认值0,虽然最终的结果都是把key为null的数据方法类数组下标为0的位置,但是1.8的这种方式就更为简洁一些。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }1.7为数组+链表;1.8为数组+链表/红黑树。
1.7为创建HashMap的时候就初始化;1.8为在使用put操作的时候才进行初始化。
1.7为Entry,1.8为Node,并且,1.7的Entry为单链表结构,1.8的Node在元素个数小于8的时候为链表,当Node元素个数大于等于8的时候就会转化为红黑树。
1.7为单独写了一个方法来处理,对于key为null的操作是直接对数组下标为0的元素进行操作;1.8则是给key为null的key值一个默认的hash值,默认值为0,同样也是会把key为null的数据放到数组下标为0的位置。
默认容量都是16,默认加载因子都是0.75;扩容方式都是两倍扩容。
1.8的HashMap做了一些优化处理,扩容时候数据的转移从头插法改为尾插法;加了红黑树结构,防止特殊情况下链表过长导致性能较低的问题;对数组的创建使用了懒加载的模式,在使用的时候才创建数组。
至于本篇为什么没有讲解红黑树,原因是红黑树有点绕,能力有限讲不清楚。