HashMap底层原理及常见问题解答

一.HashMap几个重要参数

HashMap最大支持容量:2^30;

treeify_threshold:桶的链表长度大于该值,转化为红黑树

桶中Node被树化的最小hash表容量:64 (hash表长度大于64桶链表才会转化为红黑树,当桶链表长度大于threshold时,若容量小于64,优先进行扩容)

table:存储元素的数组,长度为2的幂

size:HashMap存在的键值对的数量

threshold:扩容的临界值 = (loadFactor * size),超过该值,进行扩容

loadfactor:加载因子

二.HashMap数据结构

  • JDK1.7:数组+链表

  • JDK1.8: 引入了红黑树,在链表长度大于8时用红黑树,小于8时红黑树又退化成链表

1.初始大小:HashMap默认初始大小为16;

注:可以预估数据量大小来设置初始大小,这样可以减少动态扩容的次数,提高HashMap的性能;

2.动态扩容:最大装载因子默认为0.75,当元素个数超过0.75*capacity(Hash表的容量),会启动扩容,扩容为原来的两倍大小

3.Hash函数

int hash(Object key){
    int h = key.hashCode();
    return(h ^ (h >>> 16)) & (capicity - 1);
}

4.缺点:

  1. 存不了大数据,多次扩容性能下降

  2. 线程不安全,不支持多线程,多线程采用ConcurrentHashMap

三.相关问题:

1.HashMap的工作原理(put和get操作过程)
  • HashMap在JDK1.7中采用数组+链表
  • HashMap在JDK1.8中引入了红黑树,在链表长度大于8且Hash表长度大于64时用红黑树
  • HashMap采用get和put方法进行数据的读取和存储
  • put(key,value)方法:
    • 1.首先通过对key进行hash方法求出对应的哈希值,然后结合数组的长度得出数组的下标;
    • 2.如果数组大小超过扩容临界值,需要进行扩容,如果没有则不需要进行下一步;
      • 如果数组该位置为空,则直接将该位置的键值对信息更新,完成。
      • 如果该数组位置已有元素,则对比该位置链表上所有元素的hash是否存在
        • 如果存在,则用equals方法进行比较,结果为true时,更新键值对,结果为false,则插入到链表或者红黑树的尾部
        • 如果不存在,则执行则插入到链表或者红黑树的尾部;
    • 4.JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法
  • get(key)方法
    • 计算key的hash值,并结合数组长度得到该键值对应的下标,遍历所在下标的链表或者红黑树,用equals方法查找相同的k对应的value并返回
2.什么是Hash碰撞?

当两个对象进行哈希操作后得到的数组下标相等时,就发生了哈希碰撞

3.当两个对象的 hashCode 相同会发生什么?

因为 hashCode 相同,不一定就是相等的(equals方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。

4.JDK1.7和1.8中HashMap的区别在哪里?
  1. new HashMap()的过程中,底层原理中:JDK1.7创建一个长度为16的数组,JDK1.8没有创建,首次put时才会创建
  2. 底层数组JDK1.7Entry[]数组,JDK1.8为Node[]数组
  3. HashMap在JDK1.7中采用数组+链表,在JDK1.8中引入了红黑树,在链表长度大于8且Hash表长度大于64时用红黑树
5.为什么链表长度大于8才用红黑树?
  • 红黑树左旋右旋需要消耗性能,如果在链表长度比较短的情况下,直接用链表性能更好,因此链表长度在比较短的时候采用原始链表即可,数据量较小的时候,红黑树维持平衡的性能成本比起链表上,性能优势并不明显。
6.为什么不用二叉查找树?
  • 红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会退化成链表的线性结构,遍历查询效率低。
  • 红黑树是一种平衡树,在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,保证二叉树的深度不会太深;
  • 引入红黑树就是为了查找数据快,解决链表查询深度的问题。
7.hash 的实现?为什么要这样实现?
  • JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。
8.为什么要用异或运算符?

保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。

9.为什么HashMap是线程不安全的?

首先HashMap是线程不安全的,其主要体现:

  1. 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
  2. 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
10.Hashtable,ConcurrentHashMap和HashMap有什么区别?
  • Hashtable采用syncronized关键字,很影响插入性能,性能差
  • ConcurrentHashMap采用分段锁(1.7),
11.HashMap扩容的过程?

触发扩容的条件:当hashmap中的键值对的个数>阈值threshold时,会进行扩容;

  • jdk1.7的hashmap扩容时需要重新计算每个元素的hash,并重新确认位置;
  • jdk1.8中如果该桶没有链表只有一个元素,则和jdk1.7一样直接计算下标放置元素到新数组中,如果该桶有链表则采用优化算法进行拆分。
12.HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?
  1. table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30;

  2. loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75;

  3. 扩容时,调用 resize() 方法,将 table 长度变为原来的两倍;

  4. 如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。

13.HashMap的数组长度为什么要保证是2的幂?
  • 假如数组长度不为2的幂,那么(n-1)的二进制串的低位中肯定会有0,因此,不论hash低位上的值是0还是1,结果都会为0。这样就造成了一个性能问题:存在不同的hash值会得出相同的下标。也就是数组上的某些位置经常被拿来用,而某些位置则可能永远没被用。hashmap数组上的元素分布不均匀。
14.HashMap和Hashtable有什么区别?
  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法经过 synchronized 修饰。
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高。另外,HashTable 基本被淘汰,不要在代码中使用它;
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不同:
    1. 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
    2. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制;
15.对红黑树的理解?
  1. 1每个节点非红即黑,2根节点总是黑色的,3每个叶子节点都是黑色的空节点(NIL节点),4从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点;
  2. 红黑树在查找方面和AVL树操作几乎相同。但是在插入和删除操作上,AVL树每次插入删除会进行大量的平衡度计算,红黑树是牺牲了严格的高度平衡的优越条件为代价,它只要求部分地达到平衡要求,结合变色,降低了对旋转的要求,从而提高了性能。红黑树能够以O(log2 n)的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。
  3. 相比于BST,因为红黑树可以能确保树的最长路径不大于两倍的最短路径的长度,所以可以看出它的查找效果是有最低保证的。在最坏的情况下也可以保证O(logN)的,这是要好于二叉查找树的。因为二叉查找树最坏情况可以让查找达到O(N)。
16.HashMap,LinkedHashMap,TreeMap 有什么区别?

LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;

TreeMap 能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)

17.谈谈ConcurrentHashMap

线程安全的实现方式:

  • JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。

  • JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值