HashMap
Created At : 2023-07-25 22:31
Views 👀 :
数据结构 在 JDK1.8 中,HashMap 是由 数组+链表+红黑树构成(1.7版本是数组+链表) 当一个值中要存储到HashMap中的时候会根据Key的值来计算出他的hash,通过hash值来确认存放到数组中的位置,如果发生hash冲突就以链表的形式存储,当链表过长的话,HashMap会把这个链表转换成红黑树来存储。
基本属性 成员变量 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;static final int MAXIMUM_CAPACITY = 1 << 30 ;static final float DEFAULT_LOAD_FACTOR = 0.75 f;static final int TREEIFY_THRESHOLD = 8 ;static final int UNTREEIFY_THRESHOLD = 6 ;static final int MIN_TREEIFY_CAPACITY = 64 ;transient Node<K,V>[] table;transient Set<Map.Entry<K,V>> entrySet;transient int size ;transient int modCount;int threshold;final float loadFactor;
这里需要注意的一点是table数组并不是在构造方法里面初始化的,它是在resize(扩容)方法里进行初始化的。
Node的数据结构 static class Node <K ,V > implements Map .Entry <K ,V > { final int hash; final K key; V value; Node<K,V> next ; Node(int hash, K key, V value, Node<K,V> next ) { this .hash = hash; this .key = key; this .value = value; this .next = next ; } }
Node的数据结构是一个链表结构,红黑树也是基于Node的数据结构构建得到。TreeNode<K,V> replacementTreeNode(Node <K ,V> p, Node <K ,V> next) { return new TreeNode<> (p.hash, p.key, p.value, next); }
构造方法 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); }public HashMap (int initialCapacity ) { this (initialCapacity, DEFAULT_LOAD_FACTOR); }public HashMap ( ) { this .loadFactor = DEFAULT_LOAD_FACTOR; }public HashMap (Map <? extends K, ? extends V> m ) { this .loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false ); }
当初始容量initialCapacity大于最大容量MAXIMUM_CAPACITY大小时,设置成最大容量MAXIMUM_CAPACITY大小,防止溢出。
根据tableSizeFor获取扩容阈值。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的整数幂,结果是这个数本身的最高有效位的前一位变成1,最高有效位以及其后的位都变为0。
核心思想是,先将最高有效位以及其后的位都变为1,最后再+1,就进位到前一位变成1,其后所有的满2变0。所以关键是如何将最高有效位后面都变为1。
此时这里的阈值threshold不是初始容量*负载因子,不必在意,这只是临时的,真正设置threshold在后面put方法中。
当数组长度为2的幂次方时,可以使用位运算来计算元素在数组中的下标。 HashMap是通过index=hash&(table.length-1)
这条公式来计算元素在table数组中存放的下标,就是把元素的hash值和数组长度减1的值做一个与运算,即可求出该元素在数组中的下标,这条公式其实等价于hash%length
,也就是对数组长度求模取余,只不过只有当数组长度为2的幂次方时,hash&(length-1)才等价于hash%length
,使用位运算可以提高效率。
另外增加hash值的随机性,减少hash冲突。 如果 length 为 2 的幂次方,则 length-1 转化为二进制必定是 11111……的形式,这样的话可以使所有位置都能和元素hash值做与运算,如果是如果 length 不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在和hash 做与运算时,最后一位永远都为0 ,浪费空间。
方法 put方法 public V put (K key , V value) { // 调用putVal方法, hash(key )-计算key 的hash值 return putVal(hash(key ), key , value, false , true ); }
这里分别调用了两个方法。
hash方法 static final int hash(Object key ) { int h; return (key == null ) ? 0 : (h = key .hashCode()) ^ (h >>> 16 ); }
计算key.hashCode()并将哈希的高位数扩展到低位数。
key.hashCode()获取key的hashCode值
key的hashCode值与其无符号右移16位值进行异或^。从而让Hash值分布更均匀,右移16位就能让低16位和高16位进行异或,将高位的碰撞影响向下扩散,为了增加hash值的随机性。
putVal方法 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 ) { p.next = newNode(hash, key , value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key ) == key || (key != null && key .equals(k)))) break ; p = e; } } if (e != null ) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null ) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null ; }
putVal
方法总结归纳主要做了如下几件事:
当桶数组 table 为空时,通过扩容的方式初始化 table。
查找要插入的键值对是否已经存在,存在的话根据条件判断是否用新值替换旧值。
如果不存在,则将键值对链入链表中,并根据链表长度决定是否将链表转为红黑树。
判断键值对数量是否大于阈值,大于的话则进行扩容操作。
resize方法 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab .length; int oldThr = threshold; int new Cap , new Thr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((new Cap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) new Thr = oldThr << 1 ; } else if (oldThr > 0 ) new Cap = oldThr; else { new Cap = DEFAULT_INITIAL_CAPACITY; new Thr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (new Thr == 0 ) { float ft = (float)new Cap * loadFactor; new Thr = (new Cap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer .MAX_VALUE); } threshold = new Thr ; @SuppressWarnings({"rawtypes" ,"unchecked" }) Node<K,V>[] new Tab = (Node<K,V>[])new Node [new Cap ]; table = new Tab ; 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 ) new Tab [e.hash & (new Cap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , new Tab , j, oldCap); 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 ; new Tab [j] = loHead; } if (hiTail != null ) { hiTail.next = null ; new Tab [j + oldCap] = hiHead; } } } } } return new Tab ; }
resize
方法大致做了如下的事情:
计算新桶数组的容量 newCap 和新阈值 newThr。
根据计算出的 newCap 创建新的桶数组,桶数组 table 也是在这里进行初始化的。
将键值对节点重新映射到新的桶数组里。如果节点是 TreeNode 类型,则需要拆分红黑树。如果是普通链表节点,则节点按原顺序进行分组。
通过hash & oldCap的值来判断,若为0则索引位置不变,不为0则新索引=原索引+旧数组长度 因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap
把链表转换成红黑树,树化需要满足以下两个条件:
链表长度大于等于8
table数组长度大于等于64
get方法 public V get (Object key ) { Node<K,V> e; return (e = getNode(hash(key ), key )) == null ? null : e.value; }
getNode方法 final Node<K,V> getNode(int hash, Object key ) { Node<K,V>[] tab; Node<K,V> first , e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1 ) & hash]) != null ) { 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 ); } } return null ; }
大致逻辑如下:
根据hash值查找到指定位置的数据。
校验指定位置第一个节点的数据是key是否为传入的key,如果是直接返回第一个节点,否则继续查找第二个节点。
如果数据是TreeNode(红黑树结构),直接通过红黑树查找节点数据并返回。
如果是链表结构,循环查找所有节点,返回数据。
如果没有找到符合要求的节点,返回null。
remove方法 public V remove(Object key ) { Node<K,V> e; return (e = removeNode(hash(key ), key , null , false , true )) == null ? null : e.value; }
removeNode方法 final Node <K ,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) { Node <K ,V>[] tab; Node <K ,V> p; int n, index; //定位元素桶位置 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1 ) & hash]) != null) { Node <K ,V> node = null , e; K k; V v; // 如果键的值与链表第一个节点相等,则将 node 指向该节点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p ; else if ((e = p.next) != null) { // 如果是红黑树类型,调用红黑树的查找逻辑定位待删除节点 if (p instanceof TreeNode) node = ((TreeNode <K,V> )p).getTreeNode(hash, key); else { // 遍历链表,找到待删除节点 do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e ; break; } p = e; } while ((e = e.next) != null); } } //删除节点,并修复链表或红黑树 if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V> )node ).removeTreeNode (this, tab, movable); else if (node == p ) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node ); return node ; } } return null; }
仅需三个步骤即可完成。
定位桶位置
遍历链表找到相等的节点
第三步删除节点