HashMap与Hashtable两个类都是通过Key-Value对存储的数据结构。
根据官方的说法,二者唯二的区别是HashMap线程不安全而Hashtable线程安全,并且HashMap允许null值而Hashtable不允许。
Hashtable实现线程安全的方式是使用synchronized修饰方法,所以二者基本一致。由于Hashtable效率较低,所以Java官方不建议使用这个类了;单线程的情况下使用HashMap,多线程的时候使用ConcurrentHashMap。

一、数据结构

1. 结构

HashMap的本质是一系列的Key-Value对的集合,一个Key-Value对称为一个Entry

HashMap的基本数据结构是数组。数组中的每个非空元素(被称为/箱)都存放了一个链表或者一棵红黑树(准确来说是它们的头节点,同时这也是一个Entry)。

一个节点放置在哪个格子里,是这么决定的:首先计算出这个节点key的哈希值hh对数组长度取模,即是这个节点所在链表(或树)在数组对应的序号。

2. 扩容

数组长度初始值为16。当数组中大部分(默认是3/4)的格子中都存放了元素,说明此时哈希碰撞的几率较高了,那么就会进行扩容;每次扩容后的长度是旧长度的2倍,扩容后的长度必然是2的幂。

为什么长度要设计成2的幂?因为在哈希查找的过程中,需要对数组长度进行求模运算;而对于一个2的幂数的模数n,对其的求模运算%n可以转换为位运算&(n-1),这样可以大大提高运算效率。

3. 链表和红黑树的转换

数组中的元素初始以链表的形式保存。
当链表的长度达到阈值(默认为8),这个链表就会转换为红黑树。
当红黑树的size低于阈值(默认为6),则会重新退化为链表。

二、增删改查

1. 增/改

HashMap通过put方法添加/修改元素。put最终会调用putVal方法:

    public V put(K key, V value) {return putVal(hash(key), key, value, false, true);}final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;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);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

梳理一下往HashMap中放入K-V对的流程:

  1. 计算出key的哈希值hash并以数组长度为模求余,得到该K-V对应节点在数组中的存放位置;
  2. 如果这个格子里面没有元素,那么放入以该K-V对创建的节点作为链表的头节点;放入之后数组占用率如果达到了扩容的阈值,那么就进行扩容
  3. 如果这个格子中已经有元素了(检测到哈希碰撞):遍历这个链表或者红黑树,如果找到了key的哈希值与hash相等的节点,那么就直接更新这个节点的value(此处为修改元素);否则就将这个新节点插入到链表或者红黑树中,其中链表插入到尾部,红黑树插入到适当的位置并进行重平衡。

2. 删

删除的过程比较简单,首先计算出待删除key的哈希值hash并以数组长度为模求余,找到对应的链表/红黑树,遍历其节点;如果找到哈希值等于hash的节点,那么就删除这个节点,删除方式与链表/红黑树删除节点相同。

3. 查

计算出待查找key的哈希值,并对数组长度取模,找到对应链表或红黑树;遍历链表或搜索红黑树找到节点并返回。

4. 注意

这里对key求哈希值,并不是直接调用key.hashCode(),而是通过HashMap的hash(key)来计算的。
hash方法如下:

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

这样比起直接调用hashCode()能增强随机性,减少哈希碰撞的发生。
例如,对于浮点数,如果直接调用hashCode()会造成,一个浮点数x2x4x……2^n*x的哈希值末22位都相同,这样显然是不愿意看到的。所以HashMap使用了这个机制来保证更高的随机性。

以下代码输出都相同:
System.out.println(new Float(1.2f).hashCode() & 0x3FFFFF);
System.out.println(new Float(2.4f).hashCode() & 0x3FFFFF);
System.out.println(new Float(4.8f).hashCode() & 0x3FFFFF);
System.out.println(new Float(9.6f).hashCode() & 0x3FFFFF);

三、相关面试题

1. HashMap的特点?结构?插入过程是什么?

HashMap的特点是使用Key-Value对存储数据,无序,增删改查很快,平均时间复杂度均为O(1)。

结构是数组+链表/红黑树,数组中的每个元素保存了链表/红黑树的头节点。

插入流程:

  1. 计算出key的哈希值hash并以数组长度为模求余,得到该K-V对应节点在数组中的存放位置;
  2. 如果这个格子里面没有元素,那么放入以该K-V对创建的节点作为链表的头节点;放入之后数组占用率如果达到了扩容的阈值,那么就进行扩容
  3. 如果这个格子中已经有元素了(检测到哈希碰撞):遍历这个链表或者红黑树,如果找到了key的哈希值与hash相等的节点,那么就直接更新这个节点的value;否则就将这个新节点插入到链表或者红黑树中,其中链表插入到尾部,红黑树插入到适当的位置并进行重平衡。

2. HashMap与Hashtable的区别是什么?

  • HashMap线程不安全,Hashtable线程安全。原因是Hashtable的方法都用synchronized修饰了。
  • HashMap允许null值而Hashtable不允许。
  • HashMap继承自AbstractMap,Hashtable继承自Dictionary

3. HashMap的扩容(resize)机制讲一下?

在没有特殊设置的情况下,HashMap内部数组table的初始长度为16。当table中超过3/4(即负载因子,默认值为0.75即3/4,可以修改)的元素已经存放了节点,这时表明再添加元素就比较容易产生哈希碰撞了,这个时候table数组就需要扩容,通过resize()方法。
新的table数组为大于原数组长度的最小的2的幂数。由于**table数组长度必然是2的幂**,所以新数组的长度是原数组长度的2倍。

以上为简单回答,以下为加餐:

扩容之后,节点的位置需要重新排布,放到正确的位置组成新的链表/红黑树,那么这些节点应该放在什么地方呢?
这里使用i表示这个节点在原数组中的序号,newI表示节点在新数组中的序号。
从原理上来说,假设旧的数组长度为oldCap,新的数组长度为newCap且为旧长度的2倍。那么对于一个Entrye来说,其在新数组中的坐标newI = e.hash % newCap。一般情况下,问题本应该就此解决了,但是由于求模运算的效率不高,为了避免取模运算,Java采用了一种精妙的方式……
由求模运算的原理可以知道,inewI二者之间的关系是:newI == newI < oldCap ? i : i + oldCap
所以,newI = e.hash % newCap等价于:

if(e.hash % newCap < oldCap) {newI = i;
} else {newI = i + oldCap;
}

以旧长8,新长16为例。
假设两个节点e1e2,哈希值分别为17和25。因为对8的余数都是1,所以它们被保存在了旧数组坐标为1的元素中。当数组扩容到16时,二者对16的余数变成了1和9,所以二者在新数组中的坐标变成了1和9,相当于e1留在原地,e2往右移动了8位。

而由于oldCapnewCap是2的幂数,所以e.hash % newCap又等价于e.hash & (newCap - 1)。到了这里,就把效率低的求模运算替换为了效率极高的位运算:

if((e.hash & (newCap - 1)) < oldCap) {newI = i;
} else {newI = i + oldCap;
}

又由于newCapoldCap的2倍,也即是oldCap左移一位,e.hash & (newCap - 1)表示了e.hash末尾n位的值,其中n为oldCap的二进制位数。
又由于oldCap是二进制长度为n的最小数,如果e.hash二进制倒数第n位是1的话,那么它必然大于等于oldCap;故而**(e.hash & (newCap - 1)) < oldCap等价于:e.hash二进制倒数第n位为0。**而e.hash的倒数第n位为0,又等价于e.hash & oldCap == 0,所以上面的代码等价于:

if((e.hash & oldCap) == 0) {newI = i;
} else {newI = i + oldCap;
}

这就是Java源码中的算法。
总结来说一句话,如果节点哈希值对新数组长度求模小于旧数组长度的话,那么节点就在原地不动;否则,节点往右移动旧数组长度位。

杠精面试官:为什么负载因子是0.75而不是0.7、0.8?为什么扩容是2倍而不是1.5、2.5?

答:0.75是随手瞎jb设的,扩容2倍为了方便求模转换为位运算,如果数组长度不是2的幂,e.hash & oldCap这一步就不成立了。

4. 哈希值相同的两个对象equals一定返回true吗?反之呢?

Object类的 hashCode 是一个 native 方法。其包含三个约定:

  1. 在一次程序执行期间,如果一个对象 equals 中所比较的信息没有改变,那么 hashCode 必须返回相同值;
  2. 如果两个对象 equals 返回 true,那么 hashCode 必须相等;
  3. 如果两个对象 equals 返回 false,那么 hashCode 不需要不相等;但是为了提高 HashMap 的效率,最好不要让不同对象返回相同哈希值。

对于Object的默认实现来说,哈希值相同的两个对象,equals不一定返回true,这表明产生了哈希碰撞;equals返回true的两个对象,哈希值一定相等。
对于自行实现了equalshashCode方法的类,以实现方法为准。比如我非要把equals方法重写为return o != null && hashCode() == o.hashCode(),虽然这违反了推荐的规范,但是是可行的。

5. 为什么Java8引入了红黑树?它的优缺点是什么?

哈希碰撞是无可避免的。当最坏情况发生时,大量的节点累计在一个桶——即数组中的同一项——内。这时,HashMap的增删改查效率退化到O(n),n为链表的长度。
在链表的长度达到8时,链表会转换为红黑树。红黑树的优势为查找效率为O(logn),在冲突数据量大时效率优于链表。劣势为在建树和拆分的时候需要额外的时间、操作小步骤多、占用内存空间大,所以只有在链表长度长到足以抵消红黑树的劣势时,才进行转换。
当数组长度小于64时,即使链表长度达到8,HashMap也会优先考虑扩容而不是转红黑树。

事实上,根据概率论,在正常情况下,**如果哈希值分布比较均匀,几乎不可能出现链表转为红黑树的情况。**红黑树只是为了针对由于失误或者故意等情况造成的哈希分布不均,比如类的hashCode()方法直接返回常数。

6. HashMap线程安全吗?为什么?如何解决?

HashMap线程不安全。
原因和其他线程不安全相同,多线程操作会导致读写数据异常。
使用ConcurrentHashMap代替,或使用Collections.synchronizedMap(hashMap)包装类,或者操作时加锁,或者使用Hashtable。

7. Java7和Java8中HashMap的不同点?

Java8中加入了红黑树;
Java7链表插入方式为头插法,Java8为尾插法。
扩容时,Java7对每个元素都重新放置,Java8只对e.hash & oldCap不为0的元素移动位置。

【Java】HashMap原理及相关面试题相关推荐

  1. JAVA并发与多线程相关面试题总结

    JAVA并发与多线程相关面试题总结 1.什么是进程.线程.协程,它们之间的关系是怎样的? 进程: 本质上是一个独立执行的程序,是计算机中的程序关于数据集合上的一次运行活动,进程是操作系统进行资源分配和 ...

  2. Mybatis执行流程、缓存原理以及相关面试题

    一.Mybatis执行流程 具体分析Mybatis是如何操作数据库的! . 1.定义我们的核心配置文件的路径,这个路径是从target/classes下开始找的! String config = &q ...

  3. Java HashMap 原理

    原文:https://tryenough.com/java-hashmap 本文涉及HashMap的: HashMap的简单使用 HashMap的存储结构原理 HashMap的扩容方法原理 HashM ...

  4. Java HashMap原理及内部存储结构

    本文将通过如下简单的代码来分析HashMap的内部数据结构的变化过程. public static void main(String[] args) {Map<String, String> ...

  5. Java Web 相关面试题总结

    为什么80%的码农都做不了架构师?>>>    Spring 相关的的面试题 1.什么是 IoC 容器? 2.解释下 Spring Bean 的生命周期 IoC 容器是 Spring ...

  6. 多线程的实现方式_一文搞懂Java多线程使用方式、实现原理以及常见面试题

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  7. Hashmap 原理、源码、面试题(史上最全)

    文章很长,建议收藏起来慢慢读!疯狂创客圈总目录 语雀版 | 总目录 码云版| 总目录 博客园版 为您奉上珍贵的学习资源 : 免费赠送 :<尼恩Java面试宝典>持续更新+ 史上最全 + 面 ...

  8. 使用线程锁(lock)实现线程同步_一文搞懂Java多线程使用方式、实现原理以及常见面试题...

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点下 ...

  9. java springcloud面试题_JAVA语言之springboot+springcloud相关面试题

    本文主要向大家介绍了JAVA语言之springboot+springcloud相关面试题,通过具体的内容向大家展示,希望对大家学习JAVA语言有所帮助. 什么是springboot 用来简化sprin ...

最新文章

  1. VSLAM|回环检测之词袋字典效果测试及一点思考
  2. python所有函数用法_python函数用法总结
  3. 自研DevOps实践
  4. Robert算子的运用
  5. 最全解释P2P、P2C 、O2O 、B2C、B2B、 C2C的定义
  6. HTML 5入门学习,源码中全部使用HTML 5标签
  7. 鲲鹏920 centos7 postgresql12 postgis2.5.4编译
  8. 10以内的分解与组成怎么教_【一年级数学】(上)10以内的分与合技巧及练习题...
  9. 《目标检测》YOLO、SSD简单学习
  10. idea 设置jdk_IDEA开发小技巧~jdk问题
  11. Netty工作笔记0017---Channel和Buffer梳理
  12. JAVA创建对象有哪几种方式
  13. PHP实现生成二维码的示例代码
  14. IT 程序员、软件工程师值得考的证书有哪些?有什么证书可以考?
  15. 【HAVENT原创】使用 Spring Boot 的 AOP 全局记录执行时间日志
  16. 巴厘岛最全的美食攻略
  17. 基于Python的淘宝用户行为分析
  18. MSM8953_Linux-Android软件手册资料
  19. 苹果屏蔽更新描述文件_iOS屏蔽更新描述文件以及超级详细安装方法分享
  20. python图片保存pdf_python将JPG图片转换为PDF

热门文章

  1. 解决mac电脑打开应用“意外退出”的问题
  2. HFSS学习笔记——Vivaldi天线仿真(一)
  3. Android 网络 --framework层面 -two
  4. Linux之cp命令详解
  5. 常用的JavaScript保留小数方法
  6. 2021五款高性价比游戏蓝牙耳机推荐,喜欢吃鸡王者的小姐姐不要错过哦~
  7. 科沃斯扫地机器人电池哪里买_科沃斯扫地机器人的电池是什么电池
  8. php将数组变成字符串,php怎样将数组转成字符串
  9. 计算机桌面常见故障,电脑桌面图标常见问题
  10. 【RK3399 Ubuntu】基于rk3399的lxde的lubuntu桌面移植适配记录之第三步:Ubuntu根文件系统的桌面化分析和选用