HashMap

HashMap源码详细分析(JDK1.8)

红黑树

hashmap为什么要有负载因子?

HashMap扰动函数解读

面经手册 · 第3篇《HashMap核心知识,扰动函数、负载因子、扩容链表拆分,深度学习》

Java HashMap中在resize()时候的rehash,即再哈希法的理解

一个HashMap跟面试官扯了半个小时

Java集合之一—HashMap

HashMap

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);
    }
        /**
     * 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;
    }
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//扰动函数
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

HashMap实现原理

1.8主要的优化:

  • 数组+链表改成了数组+链表或红黑树

  • 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后

  • 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;

  • 扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小

为什么要做这几点优化;

  • 防止发生hash冲突,链表长度过长,将时间复杂度由O(n)降为O(logn);
  • 因为1.7头插法扩容时,头插法会使链表发生反转,多线程环境下会产生环;

扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢?

HashMap 中桶数组的大小 length 总是2的幂,此时,(n - 1) & hash 等价于对 length 取余。

扩容前长度为16,用于计算(n-1) & hash 的二进制n-1为0000 1111,扩容为32后的二进制就高位多了1,为0001 1111。

因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;

第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)
在这里插入图片描述
那你知道ConcurrentHashMap的分段锁的实现原理吗?

ConcurrentHashMap成员变量使用volatile 修饰,免除了指令重排序,同时保证内存可见性,另外使用CAS操作和synchronized结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。

链表转红黑树是链表长度达到阈值,这个阈值是多少?

阈值是8,红黑树转链表阈值为6

哈希冲突

当输入两个不同值,根据同一散列函数计算出相同的散列值的现象,我们就把它叫做碰撞(哈希碰撞)。

哈希冲突的常用解决方案有以下 4 种

  • 开放定址法:当关键字的哈希地址 p=H(key)出现冲突时,以 p 为基础,产生另一个哈希地址 p1,如果 p1 仍然冲突,再以 p 为基础,产生另一个哈希地址 p2,循环此过程直到找出一个不冲突的哈希地址,将相应元素存入其中。
  • 再哈希法:这种方法是同时构造多个不同的哈希函数,当哈希地址Hi=RH1(key)发生冲突时,再计算 Hi=RH2(key),循环此过程直到找到一个不冲突的哈希地址,这种方法唯一的缺点就是增加了计算时间。
  • 链地址法:这种方法的基本思想是将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。
  • 建立公共溢出区:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

HashMap 使用哪种方法来解决哈希冲突(哈希碰撞)

HashMap 使用链表和红黑树来解决哈希冲突,详见本文 put() 方法的执行过程。

HashMap 的扩容为什么是 2^n ?

这样做的目的是为了让散列更加均匀,从而减少哈希碰撞,以提供代码的执行效率。

为什么重写 equals() 时一定要重写 hashCode()?

因为 Java 规定,如果两个对象 equals 比较相等(结果为 true),那么调用hashCode 也必须相等。

HashMap 在 JDK 7 多线程中使用会导致什么问题?

HashMap 在 JDK 7 中会导致死循环的问题。因为在 JDK 7 中,多线程进行HashMap 扩容时会导致链表的循环引用,这个时候使用 get() 获取元素时就会导致死循环,造成 CPU 100% 的情况。

HashMap 在 JDK 7 和 JDK 8 中有哪些不同?

存储结构:JDK 7 使用的是数组 + 链表;JDK 8 使用的是数组 + 链表 +红黑树

存放数据的规则:JDK 7 无冲突时,存放数组;冲突时,存放链表;JDK 8在没有冲突的情况下直接存放数组,有冲突时,当链表长度小于 8 时,存放在单链表结构中,当链表长度大于 8 时,树化并存放至红黑树的数据结构中。

插入数据方式:JDK 7 使用的是头插法(先将原位置的数据移到后 1 位,再插入数据到该位置);JDK 8 使用的是尾插法(直接插入到链表尾部/红黑树)。

HashMap 的遍历方式都有几种

		Map<String, String> hashMap = new HashMap<>();
        hashMap.put("name", "老王");
        hashMap.put("sex", "你猜");
        //  方式一: entrySet  遍历
        for (Map.Entry item : hashMap.entrySet()) {
            System.out.println(item.getKey() + ":" + item.getValue());
        }
        //  方式二: iterator  遍历
        Iterator<Map.Entry<String, String>> iterator = hashMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<String, String> entry = iterator.next();
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
        //  方式三:遍历所有的 key  和 value
        for (Object k : hashMap.keySet()) {
            //  循环所有的 key
            System.out.println(k);
        }
        for (Object v : hashMap.values()) {
            //  循环所有的值
            System.out.println(v);
        }
        //  方式四:通过 key  值遍历
        for (Object k : hashMap.keySet()) {
            System.out.println(k + ":" + hashMap.get(k));
        }

HashMap、LinkedHashMap 和 TreeMap 三个映射类基于不同的数据结构,并实现了不同的功能。

HashMap 底层基于拉链式的散列结构,并在 JDK 1.8 中引入红黑树优化过长链表的问题。基于这样结构,HashMap 可提供高效的增删改查操作。LinkedHashMap 在其之上,通过维护一条双向链表,实现了散列数据结构的有序遍历。TreeMap 底层基于红黑树实现,利用红黑树的性质,实现了键值对排序功能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

德玛西亚!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值