热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

HashMap面试题(一)

HashMap面试题目录HashMap面试题一、HashMap扩容机制原理1、JDK1.7版本扩容机制JDK1.7resize方法源码initHashSeedAsNeeded()方

HashMap面试题

目录



  • HashMap面试题

    • 一、HashMap扩容机制原理

      • 1、JDK1.7版本扩容机制

        • JDK1.7 resize方法源码

        • initHashSeedAsNeeded()方法源码



      • 2、JDK1.8版本扩容机制

        • 方法执行流程

        • java8 resize方法源码



      • 3、JDK1.7和JDK1.8的区别



    • 二、HashMap和HashTable的区别?底层实现是什么?

      • 1、HashMap和HashTable的区别

      • 2、底层实现是什么






一、HashMap扩容机制原理


1、JDK1.7版本扩容机制

生成一个新的数组,将原数组的元素全都转移到新的数组上


JDK1.7 resize方法源码

/**
* 分析:resize(2 * table.length)
* 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍)
*/
void resize(int newCapacity) {
// 1. 保存旧数组(old table)
Entry[] oldTable = table;
// 2. 保存旧容量(old capacity ),即数组长度
int oldCapacity = oldTable.length;
// 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出
if (oldCapacity == MAXIMUM_CAPACITY) {
// 修改扩容阀值
threshold = Integer.MAX_VALUE;
return;
}
// 4. 根据新容量(2倍容量)新建1个数组,即新table
Entry[] newTable = new Entry[newCapacity];
// 5. 将旧数组上的数据(键值对)转移到新table中,从而完成扩容。initHashSeedAsNeeded(newCapacity)这个方法用来根据新的数组长度来重新初始化Hash种子
transfer(newTable, initHashSeedAsNeeded(newCapacity));
// 6. 新数组table引用到HashMap的table属性上
table = newTable;
// 7. 重新设置阈值,如果阈值超过了HashMap最大容量大小,则直接将阈值设置为 MAXIMUM_CAPACITY + 1
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

initHashSeedAsNeeded()方法源码

这个方法在JDK1.7中inflateTable()初始化哈希表方法和resize()扩容方法中都有出现。这个方法作用是初始化Hash种子。在JDK1.7中计算hash值的方法需要使用Hash种子来参与运算,进而提高计算出来的hash值的散列性,最大限度减少哈希冲突。下面就来简单讲一下这个方法:

* 这个方法用来根据新的数组长度来重新初始化Hash种子,好的Hash种子能提高计算Hash时结果的散列性,最大限度减少哈希冲突。
* @param capacity 根据传入的容量大小来进行重新初始化Hash种子
* @return 返回true说明已经根据传入的容量大小重新初始化了Hash种子,此时以前根据旧的Hash种子计算出来的Hash值就需要进行rehash了。
* 返回false说明并没有根据传入的容量大小进行重新初始化Hash种子
*/
final boolean initHashSeedAsNeeded(int capacity) {
// 首先会判断hashSeed是否不等于0,因为hashSeed一开始是0,所以此处是false
boolean currentAltHashing = hashSeed != 0;
// 这行代码是判断vm是否启动 且 容量到达一个值ALTERNATIVE_HASHING_THRESHOLD,这个值是可以自己去设定,不设定的话是默认的Integer.MaxValue 。 假设我们初始化容量capacity = 16,设置ALTERNATIVE_HASHING_THRESHOLD值为 3,那么这行代码会为true
boolean useAltHashing = sun.misc.VM.isBooted() &&
(capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
//亦或 ^ 的意思是 不相同则返回true,此时switching=true,那么hashSeed就会重新去计算hash种子,以便计算hash时增加散列性,
boolean switching = currentAltHashing ^ useAltHashing;
if (switching) {
// 重新设置了Hash种子
hashSeed = useAltHashing
? sun.misc.Hashing.randomHashSeed(this)
: 0;
}
return switching;
}

final int hash(Object k) {
// 设置了哈希种子
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
// Hash种子参与到了key的Hash值计算当中
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

2、JDK1.8版本扩容机制


方法执行流程

resize()****方法执行流程


java8 resize方法源码

/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* 初始化或把table容量翻倍。如果table是空,则根据threshold属性的值去初始化HashMap的容
* 量。如果不为空,则进行扩容,因为我们使用2的次幂来给HashMap进行扩容,所以每个桶里的元素
* 必须保持在原来的位置或在新的table中以2的次幂作为偏移量进行移动
* @return 返回Node数组
*/
final Node[] resize() {
// 创建一个临时变量,用来存储当前的table
Node[] oldTab = table;
// 获取原来的table的长度(大小),判断当前的table是否为空,如果为空,则把0赋值给新定义的oldCap,否则以table的长度作为oldCap的大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 创建临时变量用来存储旧的阈值,把旧table的阈值赋值给oldThr变量
int oldThr = threshold;
// 定义变量newCap和newThr来存放新的table的容量和阈值,默认都是0
int newCap, newThr = 0;
// 判断旧容量是否大于0
if (oldCap > 0) {
// 判断旧容量是否大于等于 允许的最大值,2^30
if (oldCap >= MAXIMUM_CAPACITY) {
// 以int的最大值作为原来HashMap的阈值,这样永远达不到阈值就不会扩容了
threshold = Integer.MAX_VALUE;
// 因为旧容量已经达到了最大的HashMap容量,不可以再扩容了,将阈值变成最大值之后,将原table返回
return oldTab;
}
// 如果原table容量不超过HashMap的最大容量,将原容量*2 赋值给变量newCap,如果newCap不大于HashMap的最大容量,并且原容量大于HashMap的默认容量
else if ((newCap = oldCap <<1) oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将newThr的值设置为原HashMap的阈值*2
newThr = oldThr <<1; // double threshold
}
// 如果原容量不大于0,即原table为null,则判断旧阈值是否大于0
else if (oldThr > 0) // 如果原table为Null且原阈值大于0,说明当前是使用了构造方法指定了容量大小,只是声明了HashMap但是还没有真正的初始化HashMap(创建table数组),只有在向里面插入数据才会触发扩容操作进而进行初始化
// 将原阈值作为容量赋值给newCap当做newCap的值。由之前的源码分析可知,此时原阈值存储的大小就是调用构造函数时指定的容量大小,所以直接将原阈值赋值给新容量
newCap = oldThr;
// 如果原容量不大于0,并且原阈值也不大于0。这种情况说明调用的是无参构造方法,还没有真正初始化HashMap,只有put()数据的时候才会触发扩容操作进而进行初始化
else { // zero initial threshold signifies using defaults
// 则以默认容量作为newCap的值
newCap = DEFAULT_INITIAL_CAPACITY;
// 以初始容量*默认负载因子的结果作为newThr值
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 经过上面的处理过程,如果newThr值为0,说明上面是进入到了原容量不大于0,旧阈值大于0的判断分支。需要单独给newThr进行赋值
if (newThr == 0) {
// 临时阈值 = 新容量 * 负载因子
float ft = (float)newCap * loadFactor;
// 设置新的阈值 保证新容量小于最大总量 阈值要小于最大容量,否则阈值就设置为int最大值
newThr = (newCap (int)ft : Integer.MAX_VALUE);
}
// 将新的阈值newThr赋值给threshold,为新初始化的HashMap来使用
threshold = newThr;
// 初始化一个新的容量大小为newCap的Node数组
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
// 将新创建的数组赋值给table,完成扩容后的新数组创建
table = newTab;
// 如果旧table不为null,说明旧HashMap中有值
if (oldTab != null) {
// 如果原来的HashMap中有值,则遍历oldTab,取出每一个键值对,存入到新table
for (int j = 0; j // 创建一个临时变量e用来指向oldTab中的第j个键值对,
Node e;
// 将oldTab[j]赋值给e并且判断原来table数组中第j个位置是否不为空
if ((e = oldTab[j]) != null) {
// 如果不为空,则将oldTab[j]置为null,释放内存,方便gc
oldTab[j] = null;
// 如果e.next = null,说明该位置的数组桶上没有连着额外的数组
if (e.next == null)
// 此时以e.hash&(newCap-1)的结果作为e在newTab中的位置,将e直接放置在新数组的新位置即可
newTab[e.hash & (newCap - 1)] = e;
// 否则说明e的后面连接着链表或者红黑树,判断e的类型是TreeNode还是Node,即链表和红黑树判断
else if (e instanceof TreeNode)
// 如果是红黑树,则进行红黑树的处理。将Node类型的e强制转为TreeNode,之所以能转换是因为TreeNode 是Node的子类
// 拆分树,具体源码解析会在后面的TreeNode章节中讲解
((TreeNode)e).split(this, newTab, j, oldCap);
// 当前节不是红黑树,不是null,并且还有下一个元素。那么此时为链表
else { // preserve order
/*
这里定义了五个Node变量,其中lo和hi是,lower和higher的缩写,也就是高位和低位,
因为我们知道HashMap扩容时,容量会扩到原容量的2倍,
也就是放在链表中的Node的位置可能保持不变或位置变成 原位置+oldCap,在原位置基础上又加了一个数,位置变高了,
这里的高低位就是这个意思,低位指向的是保持原位置不变的节点,高位指向的是需要更新位置的节点
*/
// Head指向的是链表的头节点,Tail指向的是链表的尾节点
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
// 指向当前遍历到的节点的下一个节点
Node next;
// 循环遍历链表中的Node
do {
next = e.next;
/*
如果e.hash & oldCap == 0,注意这里是oldCap,而不是oldCap-1。
我们知道oldCap是2的次幂,也就是1、2、4、8、16...转化为二进制之后,
都是最高位为1,其它位为0。所以oldCap & e.hash 也是只有e.hash值在oldCap二进制不为0的位对应的位也不为0时,
才会得到一个不为0的结果。举个例子,我们知道10010 和00010 与1111的&运算结果都是 0010 ,
但是110010和010010与10000的运算结果是不一样的,所以HashMap就是利用这一点,
来判断当前在链表中的数据,在扩容时位置是保持不变还是位置移动oldCap。
*/
// 如果结果为0,即位置保持不变
if ((e.hash & oldCap) == 0) {
// 如果是第一次遍历
if (loTail == null)
// 让loHead = e,设置头节点
loHead = e;
else
// 否则,让loTail的next = e
loTail.next = e;
// 最后让loTail = e
loTail = e;
}
/*
其实if 和else 中做的事情是一样的,本质上就是将不需要更新位置的节点加入到loHead为头节点的低位链表中,将需要更新位置的节点加入到hiHead为头结点的高位链表中。
我们看到有loHead和loTail两个Node,loHead为头节点,然后loTail是尾节点,在遍历的时候用来维护loHead,即每次循环,
更新loHead的next。我们来举个例子,比如原来的链表是A->B->C->D->E。
我们这里把->假设成next关系,这五个Node中,只有C的hash & oldCap != 0 ,
然后这个代码执行过程就是:
第一次循环: 先拿到A,把A赋给loHead,然后loTail也是A
第二次循环: 此时e的为B,而且loTail != null,也就是进入上面的else分支,把loTail.next =
B,此时loTail中即A->B,同样反应在loHead中也是A->B,然后把loTail = B
第三次循环: 此时e = C,由于C不满足 (e.hash & oldCap) == 0,进入到了我们下面的else分支,其
实做的事情和当前分支的意思一样,只不过维护的是hiHead和hiTail。
第四次循环: 此时e的为D,loTail != null,进入上面的else分支,把loTail.next =
D,此时loTail中即B->D,同样反应在loHead中也是A->B->D,然后把loTail = D
*/
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 遍历结束,即把table[j]中所有的Node处理完
// 如果loTail不为空,也保证了loHead不为空
if (loTail != null) {
// 此时把loTail的next置空,将低位链表构造完成
loTail.next = null;
// 把loHead放在newTab数组的第j个位置上,也就是这些节点保持在数组中的原位置不变
newTab[j] = loHead;
}
// 同理,只不过hiHead中节点放的位置是j+oldCap
if (hiTail != null) {
hiTail.next = null;
// hiHead链表中的节点都是需要更新位置的节点
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最后返回newTab
return newTab;
}

3、JDK1.7和JDK1.8的区别

img


二、HashMap和HashTable的区别?底层实现是什么?


1、HashMap和HashTable的区别

(1)最基本的区别HashTable是线程安全的,HashMap是线程不安全的,因为HashTable的每一个方法都使用synchronized关键字修饰。HashTable效率低下,现已不常使用,多使用CurrentHashMap.

(2)HashMap允许key和value为null,而HashTable不允许


2、底层实现是什么

jdk8开始链表高度到8、数组长度超过64,链表转换为红黑树,元素以内部类Node结点存在



  • 计算key的hash值,二次hash然后对数组长度取模,对应到数组下标



  • 如果没有产生hash冲突(下标位置没有元素),则直接创建Node数组



  • 如果产生hash冲突,先进行equal比较,相同则取代元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表



  • key为null,存在下标0的位置





推荐阅读
  • 本文深入探讨了CGLIB BeanCopier在Bean对象复制中的应用及其优化技巧。相较于Spring的BeanUtils和Apache的BeanUtils,CGLIB BeanCopier在性能上具有显著优势。通过详细分析其内部机制和使用场景,本文提供了多种优化方法,帮助开发者在实际项目中更高效地利用这一工具。此外,文章还讨论了CGLIB BeanCopier在复杂对象结构和大规模数据处理中的表现,为读者提供了实用的参考和建议。 ... [详细]
  • 本文探讨了 Java 中 Pair 类的历史与现状。虽然 Java 标准库中没有内置的 Pair 类,但社区和第三方库提供了多种实现方式,如 Apache Commons 的 Pair 类和 JavaFX 的 javafx.util.Pair 类。这些实现为需要处理成对数据的开发者提供了便利。此外,文章还讨论了为何标准库未包含 Pair 类的原因,以及在现代 Java 开发中使用 Pair 类的最佳实践。 ... [详细]
  • 本文深入解析了JDK 8中HashMap的源代码,重点探讨了put方法的工作机制及其内部参数的设定原理。HashMap允许键和值为null,但键为null的情况只能出现一次,因为null键在内部通过索引0进行存储。文章详细分析了capacity(容量)、size(大小)、loadFactor(加载因子)以及红黑树转换阈值的设定原则,帮助读者更好地理解HashMap的高效实现和性能优化策略。 ... [详细]
  • 使用Maven JAR插件将单个或多个文件及其依赖项合并为一个可引用的JAR包
    本文介绍了如何利用Maven中的maven-assembly-plugin插件将单个或多个Java文件及其依赖项打包成一个可引用的JAR文件。首先,需要创建一个新的Maven项目,并将待打包的Java文件复制到该项目中。通过配置maven-assembly-plugin,可以实现将所有文件及其依赖项合并为一个独立的JAR包,方便在其他项目中引用和使用。此外,该方法还支持自定义装配描述符,以满足不同场景下的需求。 ... [详细]
  • 在Ubuntu上安装MySQL时解决缺少libaio.so.1错误及libaio在MySQL中的重要性分析
    在Ubuntu系统上安装MySQL时,遇到了缺少libaio.so.1的错误。本文详细介绍了如何解决这一问题,并深入探讨了libaio库在MySQL性能优化中的重要作用。对于初学者而言,理解这些依赖关系和配置步骤是成功安装和运行MySQL的关键。通过本文的指导,读者可以顺利解决相关问题,并更好地掌握MySQL在Linux环境下的部署与管理。 ... [详细]
  • Java中不同类型的常量池(字符串常量池、Class常量池和运行时常量池)的对比与关联分析
    在研究Java虚拟机的过程中,笔者发现存在多种类型的常量池,包括字符串常量池、Class常量池和运行时常量池。通过查阅CSDN、博客园等相关资料,对这些常量池的特性、用途及其相互关系进行了详细探讨。本文将深入分析这三种常量池的差异与联系,帮助读者更好地理解Java虚拟机的内部机制。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • Java学习第10天:深入理解Map接口及其应用 ... [详细]
  • 手指触控|Android电容屏幕驱动调试指南
    手指触控|Android电容屏幕驱动调试指南 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • 本指南介绍了如何在ASP.NET Web应用程序中利用C#和JavaScript实现基于指纹识别的登录系统。通过集成指纹识别技术,用户无需输入传统的登录ID即可完成身份验证,从而提升用户体验和安全性。我们将详细探讨如何配置和部署这一功能,确保系统的稳定性和可靠性。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 2018 HDU 多校联合第五场 G题:Glad You Game(线段树优化解法)
    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=6356在《Glad You Game》中,Steve 面临一个复杂的区间操作问题。该题可以通过线段树进行高效优化。具体来说,线段树能够快速处理区间更新和查询操作,从而大大提高了算法的效率。本文详细介绍了线段树的构建和维护方法,并给出了具体的代码实现,帮助读者更好地理解和应用这一数据结构。 ... [详细]
  • 分享一款基于Java开发的经典贪吃蛇游戏实现
    本文介绍了一款使用Java语言开发的经典贪吃蛇游戏的实现。游戏主要由两个核心类组成:`GameFrame` 和 `GamePanel`。`GameFrame` 类负责设置游戏窗口的标题、关闭按钮以及是否允许调整窗口大小,并初始化数据模型以支持绘制操作。`GamePanel` 类则负责管理游戏中的蛇和苹果的逻辑与渲染,确保游戏的流畅运行和良好的用户体验。 ... [详细]
  • 本文详细探讨了使用纯JavaScript开发经典贪吃蛇游戏的技术细节和实现方法。通过具体的代码示例,深入解析了游戏逻辑、动画效果及用户交互的实现过程,为开发者提供了宝贵的参考和实践经验。 ... [详细]
author-avatar
9位特权QQ号码连号
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有