Java底层原理——HashMap面试问题

HashMap是一个基于Map接口实现的数据结构,允许null键值对,是非同步的。它通过键的hashCode方法计算存储位置,发生碰撞时使用链表或红黑树处理。当负载因子超过0.75时,HashMap会resize到两倍大小。在多线程环境下,推荐使用ConcurrentHashMap以保证线程安全。String和Integer等不可变对象适合作为键,因为它们的equals()和hashCode()已重写且不可变。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么时候会用到HashMap?他有什么特点

是基于Map接口实现的,存储键值对时,可以接收null的键值,是非同步的,HashMap存储着Entry(hash,key,value,next)对象。

你知道HashMap的工作原理吗

通过hash方法,通过put和get存储和获取对象。存储对象是,我们把键值对传递给put方法时,它调用key的hashCode方法计算hash从而得到bucket位置,进一步存储,如果发生碰撞,HashMap会通过链表将产生碰撞的元素组织起来。HashMap会根据当前bucket的占用情况自动调整容量(超过loadFactor会resize为原来的2倍)。获取对象时,将Key穿给get方法,他调用key的hashCode方法计算hash获得bucket位置,并进一步调用equals方法确定键值对。在JDK8中,如果一个bucket中冲撞的元素数量超过8时,会使用红黑树替换链表,从而提升速度

你知道get和put的原理吗?equals()和hashCode()都有什么作用?

通过key的hashCode方法进行hash,并通过(n-1&hash)的方法计算下标,从而获取buckets(桶下标)的位置,如果发生碰撞则采用key.equals()方法去链表或红黑树中查找对应的节点

你知道hash的实现吗?为什么要这样实现

在java8的实现中,是通过hashCode()的高16位异或低16位也就是扰动函数实现的。(h = key.hashCOde()) ^ (h>>>16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销

如果HashMap的大小超过了负载因子(loadFactor)定义的容量怎么办?

如果超过了负载因子,则会resize一个长度两倍的HashMap,并且在Java7中进行链表元素迁移的时候会进行rehash。而在java8中进行了一些优化,不需要进行rehash,什么优化呢?由于HashMap的长度为2的次幂所以扩容的时候需要迁移的链表元素有两种情况,要么桶下标不变,要么变为桶下标大小加上原来的HashMap的长度大小,决定一个链表元素是那种情况只在于新加入的1bit是0还是1。

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

因为hashCode相同(不代表对象地址相同),所以他们的bucket位置相同,‘碰撞’会发生。在put操作执行时发生碰撞,这个Entry会储存在链表中,如果两个对象的hashCode一样,将会通过equals()比较值决定是否采取覆盖行为(返回true),还是插入到链表中(返回false)

如果两个键的hashCode相同,你如何获取值对象

找到bucket位置后,会调用keys.equals()方法去链表找到正确的节点,最终找到要找的值对象。因此,设计HashMap的key类型时,如果使用不可变的、声明为final的对象,并且采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变能够缓存不同键的hashCode,这将提高整个获取对象的速度。、

如果HashMap的大小超过了负载因子(loadFactor)定义的容量怎么办?

默认的负载因子大小为0.75,也就是说,当一个map填满了3/4的bucket的时候,就会进行resize操作,重新创建一个大小为原来两倍的bucket数组,并将原来的对象放入新的bucket数组中,在java7时链表元素转移时会进行rehash而在java8中做了一些优化性能的操作不需要进行rehash

你了解重新调整HashMap大小存在什么问题吗?

当resize的时候确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来(java7会出现这种情况,java8做了优化不会使用头插法),因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部(同样是java7出现的情况),这是为了避免尾部遍历。如果条件竞争发生了,那么就死循环了。因此在并发环境下,我们使用CurrentHashMap来替代HashMap

为什么String,Integer这样的wrapper类适合作为建?

因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入和获取的时候返回不同的hashCode的话,那么就不能从HashMap中找到想要的对象。不可变性还有其他的优点如线程安全。见对象重写这两个方法是非常重要的。

### 关于JavaHashMap的常见面试题及解答 #### 1. **什么是HashMap?其底层数据结构是什么?** HashMap 是基于哈希表的数据结构,允许存储键值对。它的核心工作原理是通过 `hashCode()` 方法计算键的哈希码,并将其映射到桶(bucket)的位置[^2]。 - 底层数据结构由数组和链表/红黑树组成,在 Java 8 中进行了优化,当某个桶中的节点数超过一定阈值(默认为 8),该桶会从链表转换为红黑树以提高查询效率。 #### 2. **为什么要在HashMap中重写`equals()`和`hashCode()`方法?** 为了确保自定义对象作为键时能够正常工作。如果两个对象相等,则它们的哈希码应该相同;反之则不一定成立。因此,`hashCode()` 和 `equals()` 的一致性对于 HashMap 正确运行至关重要[^1]。 以下是示例代码展示如何正确实现这两个方法: ```java class Student { private String id; private String name; @Override public int hashCode() { return Objects.hash(id, name); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Student student = (Student) obj; return Objects.equals(id, student.id) && Objects.equals(name, student.name); } } ``` #### 3. **HashMap 如何处理冲突?** 当多个键被分配到同一个桶时会发生冲突。HashMap 使用拉链法解决冲突——即在同一桶中维护一个链表或红黑树来保存这些键值对。在 Java 8 及之后版本中,当链表长度达到指定阈值(默认为 8)时,链表会被替换为红黑树以提升性能[^2]。 #### 4. **HashMap 的初始容量和加载因子的作用是什么?** - **初始容量**:创建 HashMap 实例时设定的大小,默认为 16。 - **加载因子**:表示 HashMap 能够容纳多少比例的元素而不触发扩容操作,默认为 0.75。 实际容量等于 `initialCapacity * loadFactor`。一旦超出此范围,HashMap 将自动调整内部数组大小并重新分布所有条目。 模拟扩容过程如下所示: ```java public class HashMapResizeDemo { public static void main(String[] args) { // 初始容量4,负载因子0.75,阈值3 HashMap<String, Integer> map = new HashMap<>(4, 0.75f); map.put("A", 1); map.put("B", 2); map.put("C", 3); // 触发扩容 System.out.println("扩容后容量:" + map.size()); } } ``` #### 5. **遍历 HashMap 的方式有哪些?** 可以通过多种方式进行迭代访问: - KeySet 遍历 - EntrySet 遍历 - Values 遍历 具体实现可参见以下代码片段: ```java import java.util.HashMap; import java.util.Map; public class HashMapTraversalExample { public static void main(String[] args) { Map<String, Integer> map = new HashMap<>(); map.put("Alice", 90); map.put("Bob", 85); // KeySet 遍历 for (String key : map.keySet()) { System.out.println(key + ": " + map.get(key)); } // EntrySet 遍历 for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + "->" + entry.getValue()); } // Values 遍历 for (Integer value : map.values()) { System.out.println(value); } } } ``` #### 6. **HashMap 和 Hashtable 的主要区别是什么?** | 特性 | HashMap | Hashtable | |-------------------|-------------------------------------------|------------------------------------| | 线程安全性 | 不安全 | 安全 | | Null 键和值支持 | 支持 | 不支持 | | 性能 | 更高效 | 较低 | 更多细节可见参考资料[^4]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值