HashMap 那点事

HashMap

一、默认参数

// 默认初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
// 最大容量,容量必须是 2 的倍数,且小于最大容量。要是大于则取最大容量。
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子的值是 0.75,当负载因子是这个数的时候 hash 分布的更加均匀,泊松分布
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 当大于此值的时候链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
// 当小于此值的时候红黑树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;
// 控制着转化红黑树的条件,只有节点总数大于此值的时候,并且满足单个链表长度大于8才会转化红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

二、初始化那点事

HashMap的初始化种类还是挺全的,有参的,无参的,半参的都有。如果使用无参的构造函数的话,存储数据的 table 不会被初始化,只有等到真正使用到的时候才会去初始化。

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() {
    // 这里只会将负载因子赋值,并没有去 tableSizeFor
    this.loadFactor = DEFAULT_LOAD_FACTOR; 
}

tableSizeFor

这个方法是将当前值 -1 后,从最左边开始不为零的位数开始向右填充 1,最后再 + 1 完成收尾工作。这番操作下来将会得到大于 cap 的最大的 2 幂次方(7 -> 8,8 -> 8,10 -> 16)。

// 以 10 为栗子来说的话 cap = 0000 0000 0000 1010
static final int tableSizeFor(int cap) {
    int n = cap - 1; // 0000 0000 0000 1001 9
    n |= n >>> 1; // 0000 0000 0000 1101 13
    n |= n >>> 2; // 0000 0000 0000 1111 15
    n |= n >>> 4; // 15
    n |= n >>> 8; // 15
    n |= n >>> 16; // 15
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; // 16
}

三、增(put是个什么玩法)

会根据增加一个值的流程来逐步看方法

1、put

当调用 put 的方法的时候,会先 hash 得到要存放的位置,然后再进行逐步的操作

// 会将旧值返回出来,如果没有旧值则返回 null
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

2、hash 怎么跟 hashCode 还不一样?

将 key 的 hashCode 与其高 16 位的值做 ^ 操作,得到的值为真正的 hash 值,让高 16 位参与 hash 运算会减小 hash 冲突的概率。

/**
 * 1001 ^ 0010 -> 1011
 * 9 ^ 2 -> 11
 **/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

3、putVal 好长啊,还要被 put 调用

这个方法可就有点意思了,包含的知识点也比较多,首先从他的方法注解看起(翻译至方法上),至于步骤什么的都放在了方法上的注释上,这里就先总结一下 HashMap 的 put :

  1. 先做判空处理,如果为空就先扩容
  2. 根据 hash 找 table 的位置,如果找到的位置上是 nulll 的话就直接赋值
  3. 找到的位置有数据了,就循环遍历那个 链表/红黑树
  4. 如果没找到相同的 hash 与 key,则将最后一个节点的 next 指向新建的 Node,并且判断是否是大于转化成树的值,如果大于则开始使用 treeifyBin 方法尝试将链表转化为树结构,如果大小小于最小转化树结构的阈值,则进行一次扩容,而不是树结构的转化。
  5. 如果找到了的话直接结束循环
  6. 判断是找到相同值结束还是未找到值结束,如果是找到值结束则判断是否可以覆盖旧值,可以则覆盖掉,并且 return 旧值,不可以的话则直接 return 旧值
  7. 对 modCount +1,并且判断是否要扩容,需要则扩容
  8. 返回 null
  9. 流程图点击一下
/**
  * Implements Map.put and related methods.
  *
  * @param hash 		key经过 hash 方法后的值
  * @param key  		key值
  * @param value 		将要被 put 的value值
  * @param onlyIfAbsent 如果为真,则对现有的值不进行改变
  * @param evict 		如果为false,则表处于创建模式。这里在 hashmap 中并未使用,在linked 才有用
  * @return previous value, or null if none
  */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 保存数据的 Node 数组
    Node<K,V>[] tab; 
    Node<K,V> p; 
    // 用来保存 table 的长度
    int n, i;
    // 疯狂的赋值加判断,如果 table 为空则先扩容
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // n 是 2 的幂次方,所以 (n - 1) & hash 相当于 hash % n
    // 如果这里取得值是null的话,说明此空间还没有被占领,可以直接创建一个 Node 并且赋值给当前位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        // 取到的内容不为空的情况,e属于中间值
        Node<K,V> e; 
        K k;
        // 如果 hash 相同,并且 key 也相同的话,就直接将值覆盖掉
        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);
                    // 如果链表长度大于等于 8 的话,就开始尝试转化链表为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        // 尝试转换,如果表太小则扩容
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 结束查找与操作,当查到 hash 与 key.hashCode() 相同的对象的时候执行以下操作
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 判断是否可以覆盖,可以覆盖的话就直接去覆盖值
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 用以判断是否 fast-fail 的标志位 +1
    ++modCount;
    // 如果增加新值后的大小大于 容量*负载因子 的话就进行扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

4、resize 扩容怎么这么麻烦 ?

很巧妙的点是在对旧值迁移到新集合中 ,如果要迁移数据的 hash 与原先集合的 最高位 最高位& 运算,如果为 0 则表示不需要迁移,为 1 的话也只是将位置直接计算出来 现在的位置 + 旧集合的大小 ,不用再执行 hash 操作。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 这里都是按照 length 来进行操作的
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
       	// 最大也不能超过 Integer.MAX_VALUE 
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        // 新值等于旧值 << 1,并且与 1<<30 做对比,还要判断是否大于 16(初始大小是 16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 可以的话扩容至旧值的 2 倍
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) 
        // initial capacity was placed in threshold
        // 如果旧的阈值有的话,就直接赋值给要扩展的新的值
        newCap = oldThr;
    else {               
        // zero initial threshold signifies using defaults
        // 如果都没有的话就都取默认的值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 如果旧的数据集不为 null 的话就开始数据的转移
    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)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 这是一个很巧妙的点,将 e 的hash值与旧值做 & 操作,因为是 2倍扩容,所以如果是0的话就不需要移动table上的位置
                        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;
                        newTab[j] = loHead;
                    }
                    // 新的位置是 旧值 + 旧集合的大小
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

5、将链表尝试转化为红黑树的黑势力 treeifyBin

为啥叫做尝试性转换为 树结构 呢,因为有 MIN_TREEIFY_CAPACITY 给限制住了,如果小于次参数也只能乖乖地扩容去了

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

至此添加的逻辑也就这样了,但是又有很大的坑没有被填补,红黑树是怎么进行添加操作的呢?等以后再来填吧。

6、欣赏一下 Node 的容貌

Node 里面保存了 hash ,key 与 value ,其中 hash 是 hashCode 的高位与低位算出来的,赋值后即变为不可更改的状态,与 key 一样。

这里的 value 与 next 不为 final 是因为还需要覆盖与地址的重新指向。

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

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        ...
    }

    public final boolean equals(Object o) {
        ...
    }
}

7、其他 put 的风采

对于 put 整个集合的话,都会用到以下的这个方法

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
    int s = m.size();
    if (s > 0) {
        if (table == null) { // pre-size
            float ft = ((float)s / loadFactor) + 1.0F;
            int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                     (int)ft : MAXIMUM_CAPACITY);
            if (t > threshold)
                // 扩容至 t 的容量
                threshold = tableSizeFor(t);
        }
        else if (s > threshold)
            resize();
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            // 遍历写入,这里不会再扩容,因为已经提前扩容过了
            putVal(hash(key), key, value, false, evict);
        }
    }
}

四、删(remove 是怎么用的?)

从 remove 方法入手,看 remove 要执行哪些操作,调用哪些方法

1、remove 顶层方法

public V remove(Object key) {
    Node<K,V> e;
    // 还是先计算 hash 值,并且不会去判断 value 的值是否与被删除的值相同
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
        null : e.value;
}

2、removeNode

这个方法相比于 putVal 方法可以说还算简单一些了,也就是先找到要移除的 Node 的位置,然后去移除掉,分三种情况移除而已

  1. 如果是树结构则直接走树的移除方法
  2. 如果是链表结构,但是是 Tab 结构上的第一个节点,则直接指向第一个节点的 next 节点
  3. 如果是链表上的节点,则执行链表移除节点的方式。

只要没有真正移除数据,就不会去修改 modCount 的结构,也不会引起 fast-fail 的结果。

/**
 * Implements Map.remove and related methods.
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored
 * @param matchValue 如果为 True 的话就会匹配 value的值,只有相同才会去删除
 * @param movable if false do not move other nodes while removing
 * @return the node, or null if none
 */
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;
        // 找符合条件的(hash & key)的 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)
                // tree 的删除逻辑
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p)
                // 如果是 tab 位的第一个 Node 则直接指向下一个Node(可能为 null)
                tab[index] = node.next;
            else
                // 链表方式移除数据
                p.next = node.next;
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
        }
    }
    return null;
}

五、改(replace)

相比于增加的花里胡哨的操作,修改的操作确实是简单了很多,找到要修改的元素,如果找不到就返回 F,找到了之后将 Node 的 Value 替换成传入的 V 即可,如果调用带有旧值的方法的话,会有一个比较旧值的操作,相同才会去替换。

@Override
public boolean replace(K key, V oldValue, V newValue) {
    Node<K,V> e; V v;
    if ((e = getNode(hash(key), key)) != null &&
        ((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
        e.value = newValue;
        afterNodeAccess(e);
        return true;
    }
    return false;
}

@Override
public V replace(K key, V value) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) != null) {
        V oldValue = e.value;
        e.value = value;
        afterNodeAccess(e);
        return oldValue;
    }
    return null;
}

六、查(get*)

1、get

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

2、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 && // always check first node
            ((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;
}

/**
 * 树的查找方式
 **/
final TreeNode<K,V> getTreeNode(int h, Object k) {
    return ((parent != null) ? root() : this).find(h, k, null);
}

final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
    TreeNode<K,V> p = this;
    do {
        int ph, dir; K pk;
        TreeNode<K,V> pl = p.left, pr = p.right, q;
        if ((ph = p.hash) > h)
            p = pl;
        else if (ph < h)
            p = pr;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;
        else if (pl == null)
            p = pr;
        else if (pr == null)
            p = pl;
        else if ((kc != null ||
                  (kc = comparableClassFor(k)) != null) &&
                 (dir = compareComparables(kc, k, pk)) != 0)
            p = (dir < 0) ? pl : pr;
        // 开始递归起来了
        else if ((q = pr.find(h, k, kc)) != null)
            return q;
        else
            p = pl;
    } while (p != null);
    return null;
}

3、getOrDefault

这个方法还是能被提及一下的,特别是 map.getOrDefault(k, new ArrayList()) 的时候。先查一遍,为 null 就返回默认值。

@Override
public V getOrDefault(Object key, V defaultValue) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}

七、链表与数据之间的转换

我们都知道当 HashMap 的链表达到一定的条件的时候会转化为红黑树,但是这里的条件是属于哪种场景的,还有没有其他场景,存在转换关系?

链表转化为红黑树就一种,resize 的时候判断节点数量与链表是否达到转化的阈值,而将红黑树转化为链表却又是另外的一套逻辑。移除节点与 resize 的 split 都会有可能将树转化为链表。

final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
                          boolean movable) {
	...
    if (root == null
        || (movable
            && (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
        tab[index] = first.untreeify(map);  // too small
        return;
    }
	...
}
// 将红黑树转化为链表的方法
final Node<K,V> untreeify(HashMap<K,V> map) {
    Node<K,V> hd = null, tl = null;
    for (Node<K,V> q = this; q != null; q = q.next) {
        Node<K,V> p = map.replacementNode(q, null);
        if (tl == null)
            hd = p;
        else
            tl.next = p;
        tl = p;
    }
    return hd;
}

上方的代码中表达了红黑树转化为链表的条件,当树的根节点root、root.left、root.right、root.left.left 中任意一个为 null 的时候都会触发转化为链表的逻辑。

也就是说当节点有四个的时候,移除掉任何一个节点都会触发转化的逻辑,而当节点为 3 - 10 个之间,如果 remove 了这四个节点中的任意一个节点,同样会在下一次 remove 中将树转化为链表。

八、总结

总的来说要注意的点的数量放在整个 HashMap 方法中占比还是不算特别大的,需要重点看的还是他的 初始化的时候(二的幂次方)扩容的时候(达到阈值就要扩容了,new 一个Node数组,迁移数据)、树转化成链表的时候,整体还是不难的,也是很容易理解的,除了红黑树的添加那部分,查询反而是很简单了。

看完代码也就知道了,不安全、尾插法、容量是2的幂次方这些东西是怎么来了的。

posted @ 2020-12-20 09:23  atomFix  阅读(85)  评论(0编辑  收藏  举报