HashMap
1.7:
数组 + 链表: 数组就是为了存数值,链表是在数组的同一下表下存在的数值,就是在数组的同一个下标的同一个位置上,插入的数据位置上会冲突,所以在这个位置上会添加上链表,向下延申,在每次添加新的数值时,新的数值会采用头插法的方式在这条链表的第一位,这样插入的速度会快点,每次调用数值的时候,根据生成的hashcode 来找出这个数值的下表位置,再到这个链表上找对应的值。
解析源码:
- 无参构造:280行:
有个public 修饰的方法,说明这个方法我们可以直接用,里面调用了有参构造方法,里面定义的参数是 常量:参数1:默认初始化大小;参数2: 加载因子
- 有一个 初始化方法,是空的,329行
这个init() 方法在LinkedHashMap 中用到,因为LinkedHashMap 继承了 HashMap,在LinkedHashMap 中到时会看
- 有参构造:250行
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) // 如果设置的初始化容量小于0,就抛异常 throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) // 如果最大容量大于内置设定的最大容量,就把HashMap 里面设置的最大容量赋值,覆盖给自己设定的最大容量 initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) // 如果给的不是数字,抛异常 throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; // 赋值覆盖到HashMap的最大容量,加载因子 threshold = initialCapacity; // 赋值链表大小 init(); }
- 重载有参构造:272行
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); // 设置加载因子0.75(DEFAULT_LOAD_FACTOR) }
- put方法:486行
public V put(K key, V value) { if (table == EMPTY_TABLE) { // 判断table是不是等于空的,是空的调用下面的这个方法去初始化,解释1:下方看解释 inflateTable(threshold); // 初始化:下方解释2: } if (key == null) // 这里判断key为null,走下面分支,说明key可以为null,不然的话,下面肯定会抛异常 return putForNullKey(value); int hash = hash(key); // 对key调用hash方法,算出hashCode,解释3: int i = indexFor(hash, table.length); // 得到hashCode以后,算出数组下表 解释4: for (Entry<K,V> e = table[i]; e != null; e = e.next) { // 这个循环主要是作用是,在我们用map去put多个值时候,key一样的情况下,put方法返回的值是旧的值,不是get方法,get方法是会覆盖的,第一次put进去的value,这个逻辑遍历了链表 Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { // 在遍历链表时判断,hashCode相不相等, V oldValue = e.value; // 如果相等,就把之前链表上存在的覆盖赋值给新定义的 e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); // 这个方法主要是扩容的逻辑,解释5 return null; }
解释1:
调用的是这个数组的表格,这个Entry是在 801行 定义的,
解释2:
参数 在有参构造方法看,250行
inflateTable方法:311行
private void inflateTable(int toSize) { // Find a power of 2 >= toSize // 去找2的幂次方数, int capacity = roundUpToPowerOf2(toSize); //比如toSize是5,那2的幂次方数就是8,如果toSize是10,2的幂次方就最少是16,就会找16这个数字
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); // 重新计算阈值,扩容用的 table = new Entry[capacity]; initHashSeedAsNeeded(capacity); // hash种子 }
roundUpToPowerOf2()方法: 301行
private static int roundUpToPowerOf2(int number) { // assert number >= 0 : "number must be non-negative"; return number >= MAXIMUM_CAPACITY ? MAXIMUM_CAPACITY : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1; // 这个方法里面进行了多次右移位运算 }
highestOneBit(int i):这个在Integer.java类里面的一个方法,在1047行
public static int highestOneBit(int i) { // HD, Figure 3-1 i |= (i >> 1); i |= (i >> 2); i |= (i >> 4); i |= (i >> 8); i |= (i >> 16); return i - (i >>> 1); }
为什么进行这么多次位运算,在后面解释4 位置的末尾可以知道
解释3:356行
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 调用这个k的hashCode方法,这个k是Object对象,可以是任意参数,拿到这个hashCode和hash种子进行异或运算 // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); // 异或运算完了以后,在这里再进行右移和异或运算,原因在解释4 末尾得知 return h ^ (h >>> 7) ^ (h >>> 4); // 返回一个hashCode }
解释4:374行
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; return h & (length-1); // 这里不是取余,而是一个与操作,算出一个数,不得大于length这个参数的值 }
其实本身与运算是不能算出数组下标的,但是为什么可以算出,在计算机底层都是使用的二进制,
一个int是4个字节,一个字节是8位,用一个随便写的数模拟短的hashCode
其实我们不难发现,高位数不管多少,跟二进制15的数(0000 1111)与运算比起来,结果绝对都在0-15之间,正好符合数组下表,不会越界,
这样的算法有一个限制,就是要先把给的数组长度减1,并且一定要是2的幂次方
如果给的初始数组长度是17的话,17 - 1 = 16,(0001 0000)
这样显然是不好的,把拿到的数组下表给固定死了,要么是0位置,要么是16位置。
这就是为什么,默认数组长度HashMap给的是16,为什么要是2的幂次数的原因了。
这样用与操作,也会比取余的方法快很多。基于位运算的方式计算,都不会慢。
这也是为什么要进行那么多次位运算,如果不进行位运算,因为int类型的高位28位数不管怎么变,与运算最后的结果都是0,低运算位会有大量的一样的,太容易冲突了。所以要进行右移位运算。
解释5:877行
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { // threshold 扩容,说明这段是扩容的逻辑,判断当前hashmap大小是否大于阈值,null != table[bucketIndex] 判断的是,当前元素所在的下表有没有值,如果不为空,说明有值,才会延申链表
resize(2 * table.length); // 扩容的方法,生成一个新的数组,大小是之前的数组大小的双倍,看下面方法
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex); // 重要的是这个方法, 找下面方法
}
resize方法在 572行
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { // 判断之前数组的大小 threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; // new 出来一个新的容量的数组,也就是旧的数组大小 * 2 的大小的一个新的数组 transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 对之前的数组上的元素进行转移到新的数组上,看下面方法 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
transfer方法在589行
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (Entry<K,V> e : table) { //遍历链表每个元素 while(null != e) { // 判断是否需要链表延申 Entry<K,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); // 再次去调用indexFor方法 e.next = newTable[i]; newTable[i] = e; // 把数组下表给到新的数组的下标位置去 e = next; } } }
在895行
void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; // 取数组当前数组链表上面的元素 table[bucketIndex] = new Entry<>(hash, key, value, e); // 把得到的元素e,放到新的Entry对象里的next属性,就是把新加的元素头插法,放在数组的链表上了 size++; // size代表链表存多少个元素,因为多存了一个,所以就++ }
所以size方法,返回的就是size了,在384行
public int size() { return size; }
这样效率就非常高了,我们调用size方法时候,根本不是取循环遍历大小得到的,而是这样实现就已经知道了它的大小。
- get方法 在414行
public V get(Object key) { if (key == null) // 判断key是否是null return getForNullKey(); Entry<K,V> entry = getEntry(key); // 最主要是这个getEntry方法 return null == entry ? null : entry.getValue(); // 不是null,就返回entry对象 }
getEntry方法,在457行
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); // 通过key 得到hashCode值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; // 通过indexFor方法得到所对应的数组下表,遍历下表下的这个链表 e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // 先比较hash值,再比较key,就得到了对应的元素 return e; } return null; }
1.8:
在1.8时候的HashMap中,又加入了一个红黑树,当链表上的元素达到8个以后,就会开启红黑树;
为什么加入的是红黑树?为什么不是其他的树?因为在1.7时候只有链表,链表在插入的时候效率高,在拿数据的时候效率就没那么高了,所以在各种树的结构选型时候,红黑树比较均衡,因为我们不管要考虑插入的效率,还要考虑get时拿到数据的效率。红黑树在这两者方面都还挺不错的,比较折中,所以选择红黑树。
447行:有参构造方法: 和之前1.7一样的判断、传参,主要还是看put方法
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); }
红黑树的设置的值:
- 8的阈值:代表链表上的元素达到8个,就开启红黑树。
- 6的阈值:反向的,转为链表时的阈值。代表红黑树上的元素只有6个的时候,就会转成链表。因为数组当前下表的元素不多的时候,为了提高插入效率,比较折中。
为什么在方向转为链表时候不是8的这个阈值?
如果在8的大小这个元素数量的时候,一直删除一个,添加一个,这样子不停的进行红黑树转换,非常影响HashMap的效率。
put方法:611行
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
第一点:先对key进行hash值运算
337行:hash方法:还是1.7一样,对hashCode这个hash值进行右移,然后异或运算,只是没有1.7里面写的那么复杂,为什么?
1.7时候那么复杂只是为了提高hash运算结果的散列性,提高散列性是为了插入和查询效率,而1.8时候用了红黑树,所以就没必要进行那么复杂的运算了。不过右移和异或运算还是要有的。
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
625行:putVal方法:
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; // 申明tab; 申明一些数组下表、节点 4 if ((tab = table) == null || (n = tab.length) == 0) // 判断当前table是不是空,数组长度是不是0,是不是空的 5 n = (tab = resize()).length; // 如果是空的,初始化,用resize() 这个方法,得到n 6 if ((p = tab[i = (n - 1) & hash]) == null) // 用n-1 与运算这个hash值,得到数组下表,和1.7原理一样的,取出下标得到对应的元素,赋值,看这个元素是不是空 7 tab[i] = newNode(hash, key, value, null); // 如果当前数组的这个位置上是空的,就把这个新的节点元素放在这个对应的数组下标上,这个Node就和1.7里面的Entry是一样的; 8 else { // 如果在这个数组当前位置上找的下标位置不为空,走下面逻辑:29行 9 Node<K,V> e; K k; 10 if (p.hash == hash && 11 ((k = p.key) == key || (key != null && key.equals(k)))) // 判断key的hash值是否等于要插入的新的这个hash值, 12 e = p; // 如果相等就赋值,不等就直接走下面分支 13 else if (p instanceof TreeNode) // 如果遍历出来的数组上的元素p,判断p是不是树节点 14 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 如果是树,就去遍历这棵树 15 else { // 如果不是树,就执行下面的逻辑,遍历链表 16 for (int binCount = 0; ; ++binCount) { 17 if ((e = p.next) == null) { // 判断遍历到的是不是链表的尾节点了 18 p.next = newNode(hash, key, value, null); // 如果是尾节点,就把新的Node节点加到尾部,尾插法。。。 19 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 插入到尾部之后,在判断是不是大于设定的最大阈值-1 20 treeifyBin(tab, hash); // 如果大于,再去树化 21 break; 22 } 23 if (e.hash == hash && // 在遍历链表时候,判断hash值有没有相等的,key有没有相等的 24 ((k = e.key) == key || (key != null && key.equals(k)))) 25 break; 26 p = e; 27 } 28 } 29 if (e != null) { // existing mapping for key 这里代码和1.7的是一模一样了,返回OldValue 30 V oldValue = e.value; 31 if (!onlyIfAbsent || oldValue == null) 32 e.value = value; 33 afterNodeAccess(e); 34 return oldValue; 35 } 36 } 37 ++modCount; 38 if (++size > threshold) // 判断是否大于阈值,大的话就去扩容,执行resize() 方法 39 resize(); 40 afterNodeInsertion(evict); 41 return null; 42 }
* 就算这链表长度大于8时候,也不一定转红黑树:
树化的方法:755行
final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // MIN_TREEIFY_CAPACITY 64, 转红黑树之前,先判断tab是不是空,或者tab的长度小不小于64,再去扩容,执行下面resize 方法去扩容,没有去树化 resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { 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); } }
为什么?
因为扩容的话,也可以大范围的缩短这个链表,扩容一举两得,有更多的存储空间,并且基于更长远的路,来解决这个问题。
那为什么不一直扩容,就像1.7那样?
因为如果链表是在太大的话,要用红黑树,节省内存空间,这样做是为了中恒一下。