面试官再问你 HashMap 底层原理,就把这篇文章甩给他看 (2)

上边的第三个构造函数中,调用了 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; }

我们以传入参数为14 来举例,计算这个过程。

首先,14传进去之后先减1,n此时为13。然后是一系列的无符号右移运算。

//13的二进制 0000 0000 0000 0000 0000 0000 0000 1101 //无右移1位,高位补0 0000 0000 0000 0000 0000 0000 0000 0110 //然后把它和原来的13做或运算得到,此时的n值 0000 0000 0000 0000 0000 0000 0000 1111 //再以上边的值,右移2位 0000 0000 0000 0000 0000 0000 0000 0011 //然后和第一次或运算之后的 n 值再做或运算,此时得到的n值 0000 0000 0000 0000 0000 0000 0000 1111 ... //我们会发现,再执行右移 4,8,16位,同样n的值不变 //当n小于0时,返回1,否则判断是否大于最大容量,是的话返回最大容量,否则返回 n+1 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; //很明显我们这里返回的是 n+1 的值, 0000 0000 0000 0000 0000 0000 0000 1111 + 1 0000 0000 0000 0000 0000 0000 0001 0000

将它转为十进制,就是 2^4 = 16 。我们会发现一个规律,以上的右移运算,最终会把最低位的值都转化为 1111 这样的结构,然后再加1,就是1 0000 这样的结构,它一定是 2的n次幂。因此,这个方法返回的就是大于当前传入值的最小(最接近当前值)的一个2的n次幂的值。

put()方法详解 //put方法,会先调用一个hash()方法,得到当前key的一个hash值, //用于确定当前key应该存放在数组的哪个下标位置 //这里的 hash方法,我们姑且先认为是key.hashCode(),其实不是的,一会儿细讲 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } //把hash值和当前的key,value传入进来 //这里onlyIfAbsent如果为true,表明不能修改已经存在的值,因此我们传入false //evict只有在方法 afterNodeInsertion(boolean evict) { }用到,可以看到它是一个空实现,因此不用关注这个参数 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //判断table是否为空,如果空的话,会先调用resize扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //根据当前key的hash值找到它在数组中的下标,判断当前下标位置是否已经存在元素, //若没有,则把key、value包装成Node节点,直接添加到此位置。 // i = (n - 1) & hash 是计算下标位置的,为什么这样算,后边讲 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { //如果当前位置已经有元素了,分为三种情况。 Node<K,V> e; K k; //1.当前位置元素的hash值等于传过来的hash,并且他们的key值也相等, //则把p赋值给e,跳转到①处,后续需要做值的覆盖处理 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //2.如果当前是红黑树结构,则把它加入到红黑树 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //3.说明此位置已存在元素,并且是普通链表结构,则采用尾插法,把新节点加入到链表尾部 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { //如果头结点的下一个节点为空,则插入新节点 p.next = newNode(hash, key, value, null); //如果在插入的过程中,链表长度超过了8,则转化为红黑树 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); //插入成功之后,跳出循环,跳转到①处 break; } //若在链表中找到了相同key的话,直接退出循环,跳转到①处 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //① 此时e有两种情况 //1.说明发生了碰撞,e代表的是旧值,因此节点位置不变,但是需要替换为新值 //2.说明e是插入链表或者红黑树,成功后的新节点 if (e != null) { // existing mapping for key V oldValue = e.value; //用新值替换旧值,并返回旧值。 //oldValue为空,说明e是新增的节点或者也有可能旧值本来就是空的,因为hashmap可存空值 if (!onlyIfAbsent || oldValue == null) e.value = value; //看方法名字即可知,这是在node被访问之后需要做的操作。其实此处是一个空实现, //只有在 LinkedHashMap才会实现,用于实现根据访问先后顺序对元素进行排序,hashmap不提供排序功能 // Callbacks to allow LinkedHashMap post-actions //void afterNodeAccess(Node<K,V> p) { } afterNodeAccess(e); return oldValue; } } //fail-fast机制 ++modCount; //如果当前数组中的元素个数超过阈值,则扩容 if (++size > threshold) resize(); //同样的空实现 afterNodeInsertion(evict); return null; } hash()计算原理

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

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