其他分享
首页 > 其他分享> > HashMap30连问,彻底搞懂HashMap

HashMap30连问,彻底搞懂HashMap

作者:互联网

文章目录

一、背景知识

面试过程中面试官的死亡连:你说你看过很多源码是吗?那你说说hashmap的底层实现?什么条件下会自动扩容的?为什么要有 红黑树?什么条件下会有?扩容因子为什么是0.75有研究过吗? 下来就来彻底了解一下HashMap吧!

1、什么是Map?

Map是一种集合接口,提供了一系列操作K-V的方法,HashMap、HashTable都是Map接口的实现类。

2、什么是Hash?

  • Hash 音译为 “哈希” ,也叫 “散列” 。

  • 哈希的本质是通过哈希函数对原始数据进行有损压缩,得到固定长度的输出,即哈希值,通过哈希值唯一标识原始数据。

  • 若不同的原始数据被有损压缩后产生了相同的结果,该现象称为哈希碰撞。

「」迎接HashMap 30连,彻底搞懂HashMap

3、什么是哈希表?

哈希表是一种数据结构,它可以提供较高的存取效率。

【原理】

1、向哈希表插入元素时,会先根据哈希函数计算K对应的存储位置,再put元素;

2、查询时,根据哈希函数计算K对应的存储位置,直接访存储位置获取元素,查询效率高。

「」迎接HashMap 30连,彻底搞懂HashMap

【基本概念】

4、什么是HashMap?

HashMap继承了AbstractMap类,是Map接口的一种实现,用于存储K-V数据结构的元素,底层通过哈希表实现了较快的存取效率。

5、如何使用HashMap?

Map map = new HashMap(); // 创建HashMap对象
map.put("数学", 91 ); // 存放元素
map.put("语文", 92 ); // 存放元素
map.put("物理", 94 ); // 存放元素
int score = map.get("语文"); // 获取元素值
for(Object key : map.keySet()) { // 遍历元素
  System.out.println("科目"key + "的成绩是" + map.get(key));
}
map.remove("物理"); // 删除元素

6、HashMap有哪些核心参数?

1、默认初始化容量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16

2、最大容量

static final int MAXIMUM_CAPACITY = 1 << 30;

3、默认负载因子

static final float DEFAULT_LOAD_FACTOR = 0.75f;

4、树形化阈值

static final int TREEIFY_THRESHOLD = 8;

5、解树形化阈值

static final int UNTREEIFY_THRESHOLD = 6;

7、HashMap与HashTable的对比?

【相同点

都用于存储K-V元素

【不同点】

  1. HashMap可接受null键值和值,Hashtable则不能;
  2. HashMap线程不安全,HashTable线程安全,方法都加了synchronized;
  3. HashMap继承AbstractMap类,HashTable继承Dictionary类;
  4. HashMap的迭代器(Iterator)是fail-fast迭代器,HashTable的enumerator迭代器不是fail-fast,所以遍历时如果有线程改变了HashMap(增加或者移除元素),将会抛出
    ConcurrentModificationException。
  5. HashMap默认容量16,扩容为old2;HashTable默认容量11,扩容为old2+1。

8、HashMap和HashSet的区别?

HashSet实现Set接口,不允许集合中有重复值;HashMap实现Map接口,存储键值对,K不允许重复。

9、什么是LinkedHashMap和TreeMap?

  • LinkedHashMap内部维护一个链表,存储了K的插入顺序,迭代时按照插入顺序迭代。

  • TreeMap底层是一颗红黑树,containsKey、get、put、remove方法的时间复杂度都是
    log(n),按K的自然顺序排列(如整数的从小到大),也可指定Comparator比较函数。

二、HashMap的实现原理

10、HashMap的数据结构?

JDK1.7及之前,HashMap的内部数据结构是数组+链表。

「」迎接HashMap 30连,彻底搞懂HashMap

JDK1.8开始,当链表长度 > 8时会转化为红黑树,当红黑树元素个数 ≤ 6时会转化为链表。

11、HashMap put元素的原理?

「」迎接HashMap 30连,彻底搞懂HashMap

12、HashMap get元素的原理?

「」迎接HashMap 30连,彻底搞懂HashMap

三、红黑树

13、为什么要将链表转化为红黑树?

链表长度太长时,红黑树的存取效率比链表高。

14、链表元素超过8转化为红黑树,那为什么不是红黑树元素小于等于8转化为链表,而是小于等于6?

如果链表和红黑树互相转换的阈值固定是8,当HashMap元素个数在8左右变更时,

会导致树和链表数据结构的频繁变更,降低性能,所以中间预留buffer。

15、链表元素超过8是否一定转化为红黑树?

链表长度大于8 && 数组长度大于64,才会树形化,否则只是resize扩容。

为什么呢?因为数组小而链表长的场景,将链表转换为树治标不治本,应优先扩容数组。

三、hash计算和index计算

16、HashMap如何计算K的hash值?

hash值 = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

17、为什么不用K的hashCode值直接作为hash值,而是将hashCode值进行无符号右移16位,再异或的复杂操作?

Object的hashCode()函数返回的值是int型,值范围从-2147483648到2147483648,转化为2进制有32位,如、1011000110101110011111010011011。

使用右移、异或的操作,可以充分利用K的hashCode值高低位不同的特性,以减少hash碰撞的可能,提升查询效率。

18、HashMap如何计算K的数组下标?

index = h & (length-1)

四、HashMap的扩容

19、HashMap什么时候扩容?

  • 当元素数量超过阈值时扩容,阈值 = 数组容量 * 加载因子。
  • 数组容量默认16,加载因子默认0.75,所以默认阈值12。
  • 容量上限为1 << 30

20、为什么HashMap的容量必须是2的幂?

index = h & (length-1),2的幂次方-1都是1,可以充分利用高低位特点,减少hash冲突。

21、JDK7如何实现HashMap的扩容?

源码、

void resize(int newCapacity) {
  Entry[] oldTable = table;
  int oldCapacity = oldTable.length;
  if (oldCapacity == MAXIMUM_CAPACITY) {
    threshold = Integer.MAX_VALUE;
    return;
  }
  
  Entry[] newTable = new Entry[newCapacity];
  transfer(newTable, initHashSeedAsNeeded(newCapacity));
  table = newTable;
  threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
  int newCapacity = newTable.length;
  for (Entry<K,V> e : table) {
    while(null != e) {
      Entry<K,V> next = e.next;
      if (rehash) {
        e.hash = null == e.key ? 0 : hash(e.key);
      }
      int i = indexFor(e.hash, newCapacity);
      e.next = newTable[i];
      newTable[i] = e;
      e = next;
    }
  }
}

不想看源码 ?直接看我整理的简单流程图

「」迎接HashMap 30连,彻底搞懂HashMap

22、1.7为什么采用头插法?

刚添加的元素,被访的概率大。

23、头插法为什么会造成HashMap的死循环?

【step1】现有HashMap,table大小为2,里面有3个元素在index1处。

「」迎接HashMap 30连,彻底搞懂HashMap

【step2】有2个线程同时触发了扩容,但线程2刚启动扩容就被挂起,此时线程2内e指向了key(A),其next指向了key(B),而线程1完成了扩容。

「」迎接HashMap 30连,彻底搞懂HashMap

【step3】线程2被调度回来,线程2内当前待调整元素e指向A,所以头插A。此时e.next=B,待调整B。

「」迎接HashMap 30连,彻底搞懂HashMap

【step4】e = next指向B,所以头插B,此时e.next = table[i] ,本来应该指向C,但由于线程1已经完成了扩容,所以又指向了A。

「」迎接HashMap 30连,彻底搞懂HashMap

【step5】头插A,形成环。

「」迎接HashMap 30连,彻底搞懂HashMap

24、JDK8做了哪些改进,避免死循环?

1、链表太长转换为红黑树,减少死循环发生的可能;

2、1.8使用尾插法,在扩容时会保持链表元素原来的顺序,解决了死循环题,但解决不了线程不安全题。

25、JDK8扩容的优化点?

1、头插法改为尾插法,解决链表死循环题。

2、扩容的效率更高

扩容前index = hash&(oldTable.length-1),
扩容后index = hash&(oldTable.length*2-1)

唯一的区别是 length -1,多了一个高位1参与运算,如果hash对应的位置是0,则Node的index没变,如果hash对应位置是1,则newIndex =oldIndex + oldLength。

即得出结论、扩容后,Node的位置要么不变,要么移动odLength。

因此,在扩容时,不需要重新计算元素的hash了,只需要通过 if ((e.hash & oldCap) == 0)
判断最高位是1还是0就可以确定index,效率更高。

【线程安全】

26、HashMap线程安全吗?

HashMap非线程安全,线程安全的Map可以使用ConcurrentHashmap。

27、什么是ConcurrentHashMap?

HashMap线程不安全,多线程环境可以使用Collections.synchronizedMap、HashTable实现线程安全,但性能不佳。

ConurrentHashMap比较适合高并发场景使用。

28、ConcurrentHashMap JDK7的实现原理?

1、数据结构、Segment数组+HashEntry链表数组

ConcurrentHashMap由一个Segment数组构成(默认长度16),Segment继承自ReentrantLock,所以加锁时Segment数组元素之间相互不影响,所以可实现分段加锁,性能高。

Segment本身是一个HashEntry链表数组,所以每个Segment相当于是一个HashMap

「」迎接HashMap 30连,彻底搞懂HashMap

2、put元素原理

「」迎接HashMap 30连,彻底搞懂HashMap

3、get元素原理

「」迎接HashMap 30连,彻底搞懂HashMap

get()操作不需要加锁,是因为HashEntry的元素val和指针next都使用volatile修饰,在多线程环境下,线程A修改Node的val或新增节点时,对线程B都是可见的。

29、ConcurrentHashMap JDK8的实现原理?

1、数据结构、Node数组+链表/红黑树,类似1.8的HashMap。

摒弃Segment,使用Node数组+链表+红黑树的数据结构。

桶中的结构可能是链表,也可能是红黑树,红黑树是为了提高查找效率。

并发控制使用Synchronized和CAS来操作,整体看起来像是线程安全的JDK8 HashMap。

30连,彻底搞懂HashMap

2、存放元素原理

「」迎接HashMap 30连,彻底搞懂HashMap

3、获取元素原理

计算hash - 计算数组下标 - 遍历节点

30、JDK8对HashMap做了哪些优化?

1、引入红黑树 ,提升元素获取速度。

2、头插法改为尾插法,解决链表死循环题。

3、扩容的效率更高

标签:扩容,hash,HashMap,HashMap30,元素,链表,线程,连问,搞懂
来源: https://blog.csdn.net/Redemption___/article/details/116835615