7000 字说清楚 HashMap,面试点都在里面了 (2)

前面说了,当桶内链表到达 8 的时候,会将链表转换成红黑树,就是 TreeNode类型,它也是 HashMap中定义的静态内部类。

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; } 容量和默认容量

容量就是 table 数组的长度,也就是我们所说的桶的个数。其定义如下

int threshold;

默认是 16,如果我们在初始化的时候没有指定大小,那就是 16。当然我们也可以自己指定初始大小,而 HashMap 要求初始大小必须是 2 的 幂次方。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 元素个数

容量是指定了桶的个数,而 size 是说 HashMap中实际存了多少个键值对。

transient int size; 最大容量

table 的长度也是有限制的,不能无限大,HashMap规定最大长度为 2 的30次方。

static final int MAXIMUM_CAPACITY = 1 << 30; 负载因子

这是一个系数,它和 threshold 结合起作用,默认是 0.75。一般情况下不要改。

final float loadFactor; 扩容阈值

阈值 = 容量 x 负载因子,假设当前 HashMap的容量是 16,负载因子是默认值 0.75,那么当 size 到达 16 x 0.75= 12 的时候,就会触发扩容。

初始化 HashMap

使用 HashMap肯定要初始化吧,很多情况下都是用无参构造方法创建。

Map<String,String> map = new HashMap<>();

这种情况下所有属性都是默认值,比如容量是 16,负载因子是 0.75。

另外推荐的一种初始化方式,就是给定一个默认容量,比如指定默认容量是 32。

Map<String,String> map = new HashMap<>(32);

但是 HashMap 要求初始大小必须是 2 的 n 次方,但是又不能要求每个开发人员指定初始容量的时候都按要求来,比如我们指定初始大小为为 7、18 这种会怎么样呢?

没关系,HashMap中有个方法专门负责将传过来的参数值转换为最接近、且大于等于指定参数的 2 的 n 次方的值,比如指定大小为 7 的话,最后实际的容量就是 8 ,如果指定大小为 18的话,那最后实际的容量就是 32 。

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); }

执行这个转换动作的就是 tableSizeFor方法,经过转换后,将最终的结果赋值给 threshold变量,也就是初始容量,也就是本篇中所说的桶个数。

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; }

tableSizeFor这个方法就有意思了,先把初始参数减 1,然后连着做或等于和无符号右移操作,最后算出一个接近的 2 的幂次方,下图演示了初始参数为 18 时的一系列操作,最后得出的初始大小为 32。

image-20200614232442638

这个算法很有意思了,比如你给的初始大小是 63,那得到的结果就是 64,如果初始大小给定 65 ,那得到的结果就是 128,总是能得出不小于给定初始大小,并且最接近的2的n次方的最终值。

从 put 方法解密核心原理

put方法是增加键值对最常用的方法,也是最复杂的过程,增加键值对的过程涉及了 HashMap最核心的原理,主要包括以下几点:

什么情况下会扩容,扩容的规则是什么?

插入键值对的时候如何确定索引,HashMap可不是按顺序插入的,那样不就真成了数组了吗。

如何确保 key 的唯一性?

发生哈希碰撞怎么处理?

拉链法是什么?

单桶内的链表如何转变成红黑树?

以下是 put 方法的源码,我在其中做了注释。

public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K,V>[] tab; // 声明 Node 数组 tab HashMap.Node<K,V> p; // 声明一个 Node 变量 p int n, i; /** * table 定义 transient Node<K,V>[] table; 用来存储 Node 节点 * 如果 当前table为空,则调用resize() 方法分配数组空间 */ if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // n 总是为 2 的幂次方,(n-1) & hash 可确定 tab.length (也就是table数组长度)内的索引 // 然后 创建一个 Node 节点赋给当前索引 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //如果当前索引位置已经有值了,怎么办 // 拉链法出场 HashMap.Node<K,V> e; K k; // 判断 key 值唯一性 // p 是当前待插入索引处的值 // 哈希值一致并且(当前位置的 key == 待插入的key(注意 == 符号),或者key 不为null 并且 key.equals(k)) if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //如果当前节点只有一个元素,且和待插入key一样 则覆盖 // 将 p(当前索引)节点临时赋予 e e = p; else if (p instanceof HashMap.TreeNode) // 如果当前索引节点是一颗树节点 //插入节点树中 并返回 e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 当前索引节点即不是只有一个节点,也不是一颗树,说明是一个链表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //找到没有 next 的节点,也就是最后一个 // 创建一个 node 赋给 p.next p.next = newNode(hash, key, value, null); // 如果当前位置+1之后大于 TREEIFY_THRESHOLD 则要进行树化 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //执行树化操作 treeifyBin(tab, hash); break; } //如果又发生key冲突则停止 后续这个节点会被相同的key覆盖 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; // 当实际长度大于 threshold 时 resize if (++size > threshold) resize(); afterNodeInsertion(evict); return null; } 首次初始化数组和扩容

在执行 put方法时,第一步要检查 table 数组是否为空或者长度是否为 0,如果是这样的,说明这是首次插入键值对,需要执行 table 数组初始化操作。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zypgjp.html