hashMap的1.7和1.8的底层实现原理是不同的,需要分开讨论
【HashMap源码解读】阿里P8级别的架构师全套视频教程(2021最新版)_哔哩哔哩_bilibili(b站视频参考)
HashMap的长度为什么要是2的n次方_zs319428的博客-CSDN博客_hashmap2的n次方(HashMap的长度为什么要是2的n次方)
Java集合容器面试题(2020最新版)_ThinkWon的博客-CSDN博客_java集合(Java集合容器面试题(2020最新版))
HashMap之1.7和1.8的区别_weijian、Li的博客-CSDN博客_hashmap1.7和1.8的区别(HashMap之1.7和1.8的区别)
(1)美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析_依本多情的博客-CSDN博客_hashmap1.7和1.8的区别(美团面试题:Hashmap的结构,1.7和1.8有哪些区别,史上最深入的分析)
hashmap的如果不设定默认容量初始值是16加载因子是0.75
1.7 put详解
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); return null; }
----inflateTable方法,懒加载put的时候才去初始化表,判断长度是否为空,去初始化表table数组 Entry<K,V>[],初始化的数组的长度是2的n次方数
----如果key为null,添加key为null的节点,在链表的第一个位置
/** * Inflates the table. */ private void inflateTable(int toSize) { // Find a power of 2 >= toSize int capacity = roundUpToPowerOf2(toSize); threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); table = new Entry[capacity]; initHashSeedAsNeeded(capacity); }
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; --左移以为相当于乘以2 }
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); } // |运算的逻辑是有1则为1
如果一个数是2的次方数,那么它用二进制表示只有最高位是1,其他位都是0,highestOneBit其实就是找到小于或等于初始值的最小2次方数字,通过这种计算后就能找到最高位是1但是其他位都是0的数了。
2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
比如说16 : 1 0000 15: 0 1111
roundUpToPowerOf2 找到>=初始值的2的幂次方的数
那为什么要2的n次方幂呢??因为2的n-1用二进制标识就是0 1111111
其实就是按位“与”的时候,每一位都能 &1 ,也就是和1111……1111111进行与运算,这样能增加散列性。
----hash 算出hash值,hash方法里面的位运算移动以及异或运算时提高散列性,防止链表过长。让高位参与运算,因为容器长度时2的n此方法-1,高位是0低位是1。则h & (length-1),hashcode的高位和0位与都是0,不会参与到运算,所以将高位右移变成低位,高位也参与到运算中来了。
final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); // 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); return h ^ (h >>> 7) ^ (h >>> 4); }
----indexFor 算出数组下标
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); }
----for循环这一段就是去遍历链表,用equals方法判断是否相等,相等则覆盖,并且返回原理值
----addEntry方法添加节点与扩容有关
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); }
新的节点添加到链表头部,并且进行移动
其中的resize方法进行扩容 resize(2 * table.length),创建一个新数组,长度是原来的两倍。transfer方法遍历原有的Entry数组,将所有的元素重新Hash到新数组中
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]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
/** * Transfers all entries from current table to newTable. */ 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); e.next = newTable[i]; newTable[i] = e; e = next; } } }
hash到新数组以后是链表上的元素是逆序的,这种的话多线程的情况下可能会出现环形链表死循环问题。
1.8引入红黑树就是防止链表过长导致查询效率get变低
1.8插入元素是尾插法,为什么要用尾插法,因为既然要遍历链表判断长度是不是大于8,既然要查询为什么不直接尾插呢,1.7头插法是因为效率高,不然的话还要去遍历链表判断是不是最后一个节点,这样反而让效率变低。
Node<K,V>[] table数组是Node了,1.7是Entry[]
初始化inflateTable()和扩容resize()合并成一个resize()
hash(散列)算法:这比在JDK 1.7中,更为简洁,相比在1.7中的4次位运算,5次异或运算(9次扰动),在1.8中,只进行了1次位运算和1次异或运算(2次扰动);
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 { 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) { --- 判断是不是最后一个节点,因为next为null的肯定是最后一个节点 p.next = newNode(hash, key, value, null); --- 是的话查到尾部 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st --- 如果元素个数大于8,树话 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } 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; }