关于HashMap要知道的事

版权声明:如果觉得文章对你有用,转载不需要联系作者,但请注明出处 https://blog.csdn.net/jinxin70/article/details/85100402

HashMap数据结构

底层使用了数组+链表。

如果,链表的长度大于等于8(TREEIFY_THRESHOLD)了,则将链表改为红黑树,这是Java8 的一个新的优化。

if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
	treeifyBin(tab, hash);

当发生 哈希冲突(碰撞)的时候,HashMap 采用 拉链法 进行解决。

扩容开销很大(需要创建新数组、重新哈希、分配等等),与扩容相关的两个因素:

默认加载因子(DEFAULT_LOAD_FACTOR)是 0.75。HashMap 使用此值基本是平衡了性能和空间的取舍。

  • 加载因子太大的话发生冲突的可能就会大,查找的效率反而变低
  • 太小的话频繁 rehash,导致性能降低

加载因子决定了 HashMap 中的元素占有多少比例时扩容

初识容量(DEFAULT_INITIAL_CAPACITY)16。

HashMap扩容的时机:

容器中的元素数量 > 负载因此 * 容量,如果负载因子是0.75,容量是16,那么当容器中数量达到13 的时候就会扩容。

还有,如果某个链表长度达到了8,并且容量小于64(MIN_TREEIFY_CAPACITY),则也会用扩容代替红黑树。

/**
     * Replaces all linked nodes in bin at index for given hash unless
     * table is too small, in which case resizes instead.
     */
    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);
        }
    }

HashMap中的性能优化

HashMap 扩容的时候,不管是链表还是红黑树,都会对这些数据进行重新的散列计算,然后缩短他们的长度,优化性能。在进行散列计算的时候,会进一步优化性能,减少减一的操作,直接使用& 运算。

HashMap的重新Hash算法:

static final int hash(Object key) {
	int h;
	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

(图片来自 http://yikun.github.io/2015/04/01/Java-HashMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

避免只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,可以避免哈希值分布不均匀。

而且,采用位运算效率更高。

HashMap 如何根据 hash 值找到数组中的对象

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

关键的是其中的这一行:first = tab[(n - 1) & hash])

当n是2的指数时,上面的(n-1)&hash相当于hash%n,对于处理器来说,除法和求余比较慢,为了性能,使用了减法和按位与运算。

如何正确使用

无论我们如何设置初始容量,HashMap 都会将我们改成2的幂次方,也就是说,HashMap 的容量百分之百是 2的幂次方。

但是,请注意:如果我们预计插入7条数据,那么我们写入7,HashMap 会设置为 8,虽然是2的幂次方,但是,请注意,当我们放入第7条数据的时候,就会引起扩容,造成性能损失,所以,知晓了原理,我们以后在设置容量的时候还是自己算一下,比如放7条数据,我们还是都是设置成16,这样就不会扩容了。

非线程安全

HashMap 在 JDK 7 中并发扩容的时候是非常危险的,非常容易导致链表成环状。但 JDK 8 中已经修改了此bug。但还是不建议使用。强烈推荐并发容器 ConcurrentHashMap。

编码启示

如果参与中间件、基础架构开发,时刻追求性能是很有必要的。

HashMap如何根据指定容量设置阈值

得出最接近指定参数 cap 的 2 的 N 次方容量。假如你传入的是 5,返回的初始容量为 8 。

/**
     * Returns a power of two size for the given target capacity.
     */
    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;
    }

 可以自己举例验证,但是如果问怎么写出来的,我也不知道。只能膜拜。

二进制位运算规则参考:

<< : 左移运算符,num << 1,相当于num乘以2  低位补0
>> : 右移运算符,num >> 1,相当于num除以2  高位补0
>>> : 无符号右移,忽略符号位,空位都以0补齐
 % : 模运算 取余
^ :   位异或 第一个操作数的第n位与第二个操作数的第n位相反,那么结果的第n为也为1,否则为0
 & : 与运算 第一个操作数的第n位与第二个操作数的第n位如果都是1,那么结果的第n为也为1,否则为0
 | :  或运算 第一个操作数的第n位与第二个操作数的第n位 只要有一个是1,那么结果的第n为也为1,否则为0
 ~ : 非运算 操作数的第n位为1,那么结果的第n位为0,反之,也就是取反运算(一元操作符:只操作一个数)

参考文章:

深入理解-HashMap-put-方法(JDK-8逐行剖析)

Java 集合深入理解(16):HashMap 主要特点和关键方法源码解读

Java:手把手带你源码分析 HashMap 1.7

Java源码分析:关于 HashMap 1.8 的重大更新

Java:那些关于集合的知识都在这里了!

Java 集合深入理解(17):HashMap 在 JDK 1.8 后新增的红黑树结构

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

猜你喜欢

转载自blog.csdn.net/jinxin70/article/details/85100402
今日推荐