HashMap面试

⼩伙⼦你了解数据结构中的HashMap么?能跟我聊聊他的结构和底层原理么?

HashMap是我们非常常用的一种数据结构,由链表和数组组合构成的数据结构。

⼤概如下,数组⾥⾯每个地⽅都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node。

因为它最初所有的位置都为null,在put插入数据时会根据key的hash去计算一个index值。

例如:

比如说我put("余温",520),我插入一个"余温的元素",这个时候我们会通过hash函数计算出插入的位置,计算出index是2,因此结果如下。

hash("余温") = 2

你提到了还有链表,为啥需要链表,链表⼜是怎么样⼦的呢?

我们都知道数组⻓度是有限的,在有限的⻓度⾥⾯我们使⽤哈希,哈希本身就存在概率性,就是"余温"和"温余"我们都去hash有⼀定的概率会⼀样,就像上⾯的情况我再次哈希"温余"极端情况也会hash到⼀个值上,那就形成了链表。

每个节点都会保存自身的hash、key、value、以及下一个节点,我们看一下Node的源码

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
​
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
​
        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }
​
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
​
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
​
        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

说到链表我想问⼀下,你知道新的Entry节点在插⼊链表的时候,是怎么插⼊的么?

java8之前是头插法,意思就是新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子,因为当时写代码的这个作者认为后来的值被查找的可能性更大,提升查找的效率。

但是,在java8之后,都使用尾部插入了。

>•  为啥改为尾部插入呢?

有⼈认为是作者随性⽽为,没啥luan⽤,其实不然,其中暗藏⽞机

首先我们看下HashMap的扩容机制:

由于数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。

什么时候resize呢?

有两个因素:

  • Capacity:HashMap当前长度

  • LoadFactor:负载因子,默认值0.75f。

怎么理解呢,就比如当前的容量大小为100,当你存进第76个的时候,判断发现需要进行resize了,那就进行扩容,但是HashMap的扩容也不是简单的扩大点容量这么简单的.

扩容?它是怎么进行扩容的呢?

分为两步

  • 扩容:创建一个新的Entry空数组,长度是原数组的两倍。

  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新的数组。

为什么要重新Hash呢,直接复制过去不行吗?

是因为长度扩大之后,Hash的规则也会随之改变。

Hash的公式---> index = HashCode(Key) & (Length - 1) ​ 原来⻓度(Length)是8你位运算出来的值是2 ,新的⻓度是16你位运算出来的值明显不⼀样了。

扩容前:

扩容后:

说完扩容机制我们⾔归正传,为啥之前⽤头插法,java8之后改成尾插了呢?

先来举个例子吧,比如说我们现在向一个容量大小为2的里面put两个值,负载因⼦是0.75是不是我们在put第⼆个的时候就会进⾏resize?

2 * 0.75 = 1 所以我们在插入第二个的时候就要resize了。

现在我们要在容量为2的容器⾥⾯⽤不同线程插⼊A,B,C,假如我们在resize之前打个短点,那意味着数据都插⼊了但是还没resize那扩容前可能是这样的。 ​ 我们可以看到链表的指向A->B->C

Tip:A的下⼀个指针是指向B的

因为resize的赋值⽅式,也就是使⽤了单链表的头插⼊⽅式,同⼀位置上新元素总会被放在链表的头部位置,在旧数组中同⼀条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。

就可能出现下⾯的情况,B的下⼀个指针指向了A

⼀旦⼏个线程都调整完成,就可能出现环形链表

如果这个时候去取值,悲剧就出现了——Infinite Loop。

头插是JDK1.7的那1.8的尾插是怎么样的呢?

因为java8之后链表有红⿊树的部分,⼤家可以看到代码已经多了很多if else的逻辑判断了,红⿊树的引⼊巧妙的将原本O(n)的时间复杂度降低到了O(logn)。 ​ Tip:在这⾥过多描述了,之后专门出一篇关于红黑树的。

使⽤头插会改变链表的上的顺序,但是如果使⽤尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。 ​ 就是说原本是A->B,在扩容后那个链表还是A->B,如下图所示。

Java7在多线程操作HashMap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引⽤关系。

Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引⽤关系。

那是不是意味着Java8就可以把HashMap⽤在多线程中呢?

我认为即使不会出现死循环,但是通过源码看到put/get⽅法都没有加同步锁,多线程情况最容易出现的就是:⽆法保证上⼀秒put的值,下⼀秒get的时候还是原值,所以线程安全还是⽆法保证。

那我问你HashMap的默认初始化⻓度是多少?

我记得我在看源码的时候初始化⼤⼩是16

那你知道为什么是16么?

编辑器会提醒我们最好赋初值,而且最好是2的幂,这样为了位运算方便,位运算比算数计算的效率高很多,之所以选择16是为了服务将key映射到index的算法中; ​ 上面讲到了put的时候会hash一些key的值,但是我们怎么尽最大可能得到一个均匀分布的hash呢?我们通过的是key的hashcode值去做位运算。 例如: ​ key为上面的"余温"十进制为873775二进制为11010101010100101111 ​ HashMap下标的计算方式是index=HashCode(Key)&(Length-1) ​ 代入得 11010101010100101111&1111 十进制就是15

之所以⽤位与运算效果与取模⼀样,性能也提⾼了不少!

那为啥⽤16不⽤别的呢?

因为在使⽤不是2的幂的数字的时候,Length-1的值是所有⼆进制位全为1,这种情况下,index的结果等同于HashCode后⼏位的值。

只要输⼊的HashCode本身分布均匀,Hash算法的结果就是均匀的。 ​

这是为了实现均匀分布。

那我问你个问题,为啥我们重写equals⽅法的时候需要重写hashCode⽅法呢?

你能⽤HashMap给我举个例⼦么?

因为在java中,所有的对象都是继承于Object类。Ojbect类中有两个⽅法equals、hashCode,这两个⽅法都是⽤来⽐较两个对象是否相等的。

  • 对于值对象,==⽐较的是两个对象的值

  • 对于引⽤对象,⽐较的是两个对象的地址

在未重写equals⽅法我们是继承了object的equals⽅法,那⾥的 equals是⽐较两个对象的内存地址,显然我们new了2个对象内存地址肯定不⼀样

⼤家是否还记得我说的HashMap是通过key的hashCode去寻找index的,那index⼀样就形成链表了,也就是说"余温"和"温余"的index都可能是2,在⼀个链表上的。

我们去get的时候,他就是根据key去hash然后计算出index,找到了2,那我怎么找到具体的"余温"还是"温余"呢?

equals!是的,所以如果我们对equals⽅法进⾏了重写,建议⼀定要对hashCode⽅法重写,以保证相同的对象返回相同的hash值,不同的对象返回不同的hash值。

不然⼀个链表的对象,你哪⾥知道你要找的是哪个,到时候发现hashCode都⼀样,这不是完犊⼦嘛。

可以可以⼩伙⼦,我记得你上⾯说过他是线程不安全的,那你能跟我聊聊你们是怎么处理HashMap在线程安全的场景么?

⾯试官,在这样的场景,我们⼀般都会使⽤HashTable或者ConcurrentHashMap,但是因为前者的并发度的原因基本上没啥使⽤场景了,所以存在线程不安全的场景我们都使⽤的是ConcurrentHashMap。 ​ HashTable我看过他的源码,很简单粗暴,直接在⽅法上锁,并发度很低,最多同时允许⼀个线程访问,ConcurrentHashMap就好很多了,1.7和1.8有较⼤的不同,不过并发度都⽐前者好太多了。

总结 HashMap绝对是最常问的集合之⼀,基本上所有点都要烂熟于⼼的那种,篇幅和时间的关系,我就不多介绍了,核⼼的点我基本上都讲到了,不过像红⿊树这样的就没怎么聊了,但是不代表不重要。

HashMap常⻅⾯试题:

  • HashMap的底层数据结构?

  • HashMap的存取原理?

  • Java7和Java8的区别?

  • 为啥会线程不安全?

  • 有什么线程安全的类代替么?

  • 默认初始化⼤⼩是多少?为啥是这么多?为啥⼤⼩都是2的幂?

  • HashMap的扩容⽅式?负载因⼦是多少?为什是这么多?

  • HashMap的主要参数都有哪些?

  • HashMap是怎么处理hash碰撞的?

  • hash的计算规则?

<think>好的,我现在需要处理用户关于HashMap面试题目的查询。首先,用户已经提供了四个引用来源,其中引用[1]、[2]、[3]、[4]都涉及Java集合框架,特别是HashMap的相关内容。我需要根据这些引用来整理出常见的问题和答案。 首先,我需要回顾每个引用的内容。引用[1]提到了TreeSet、区分重复元素的方法、ArrayList和Vector的区别,以及Collection和Collections的区别。引用[2]讨论了HashMap与Hashtable的区别,以及可能的刁钻问题。引用[3]详细描述了JDK1.8前后HashMap的底层实现,包括扰动函数、拉链法和红黑树。引用[4]则是关于Java集合的20个问题,但具体内容未详细列出,不过可能包含更多相关问题。 用户的需求是查找HashMap面试题及答案,因此需要重点从引用[2]、[3]中提取信息,并结合其他引用中的相关内容。例如,引用[3]详细解释了HashMap的底层结构、哈希冲突解决方法和红黑树的引入,这些都是常见的面试问题。引用[2]提到了HashMap与Hashtable的区别,这也是经典问题。 接下来,我需要整理出常见的问题,并确保每个问题都有对应的解答,同时正确引用来源。例如,问题可能包括: 1. HashMap和Hashtable的区别?答案应涉及null键值、同步性等,引用[2]和[3]。 2. JDK1.8中HashMap的改进?涉及红黑树,引用[3]。 3. 哈希冲突解决方法?拉链法和红黑树,引用[3]。 4. 扰动函数的作用?引用[3]。 5. 为何HashMap不保证顺序?引用[3]或常识。 6. Collection和Collections的区别?引用[1]。 7. 其他相关问题如初始容量、加载因子、线程安全等,可能需综合多个引用。 需要注意每个答案后正确标注引用来源,例如关于扰动函数的部分来自引用[3]。同时,用户要求使用中文回答,行内公式用$...$,独立公式用$$...$$,但根据问题内容可能公式较少,但需注意LaTeX语法正确。 另外,用户示例中的快速排序部分有代码块,这里可能需要针对HashMap的问题加入代码示例,如扰动函数的实现,或者红黑树相关的伪代码,但需确认引用内容中是否有提供。例如,引用[3]提到扰动函数是HashMap的hash方法,可以举例代码。 最后,生成相关问题部分需要基于已整理的问题,提出扩展性问题,例如性能优化、并发处理等,可能需要结合引用中的内容或常识。 现在需要确保每个问题解答准确,引用正确,格式符合要求,并且结构清晰。可能还需要检查是否有重复问题,或者是否覆盖了用户关心的主要方面。</think>### HashMap面试问题及解答 #### 1. HashMap与Hashtable的区别是什么? - **HashMap**允许键和值为`null`,而**Hashtable**不允许[^2]。 - **HashMap**是非线程安全的(非synchronized),而**Hashtable**的方法通过`synchronized`关键字保证线程安全[^2][^3]。 - **HashMap**在JDK1.8后引入红黑树优化链表过长的问题,而**Hashtable**无此机制。 #### 2. JDK1.8中HashMap的底层实现有哪些改进? JDK1.8前,HashMap使用**数组+链表**(拉链法)处理哈希冲突;JDK1.8后,当链表长度超过阈值(默认为8)时,链表会转换为**红黑树**,以提高查询效率(时间复杂度从$O(n)$降为$O(\log n)$)。 #### 3. HashMap如何解决哈希冲突? - **拉链法**:将哈希值相同的键值对存储在链表中。 - **红黑树优化**:当链表过长时转换为红黑树(JDK1.8引入)。 #### 4. 什么是扰动函数?它的作用是什么? 扰动函数是HashMap的`hash()`方法,通过将键的哈希码的高16位与低16位异或,减少哈希冲突。例如: ```java static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } ``` 其目的是让哈希码分布更均匀,减少低质量`hashCode()`实现的影响。 #### 5. HashMap的初始容量和加载因子是什么? - **初始容量**默认为16,**加载因子**默认为0.75。当元素数量超过`容量×加载因子`时触发扩容(容量翻倍)。 - 公式:扩容阈值$T = \text{capacity} \times \text{loadFactor}$。 #### 6. 为何HashMap不保证元素的顺序? 因为哈希函数将键分散到不同的桶中,且扩容时可能重新哈希元素,导致顺序变化。 #### 7. Collection和Collections的区别是什么? - **Collection**是集合框架的根接口(如List、Set)。 - **Collections**是工具类,提供静态方法操作集合(如排序、同步包装)[^1]。 #### 8. HashMap为何线程不安全? 多个线程同时扩容可能导致链表成环或数据丢失。线程安全替代方案: - 使用`ConcurrentHashMap`。 - 通过`Collections.synchronizedMap()`包装。 #### 9. HashMap的`put()`方法流程? 1. 计算键的哈希值。 2. 定位到数组索引。 3. 若桶为空,直接插入;否则遍历链表/红黑树: - 键存在则更新值。 - 键不存在则插入新节点。 4. 若链表长度≥8且数组长度≥64,链表转红黑树。 #### 10. 为何JDK1.8引入红黑树而非其他数据结构? 红黑树在增删查操作上综合性能较好(平均时间复杂度$O(\log n)$),且能避免链表过长导致的性能骤降。 --- ###
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值