问题 1:ConcurrentHashMap
和 Collections.synchronizedMap()
有什么区别?
✅ 答案:
两者都提供线程安全的 Map,但实现方式截然不同:
• ConcurrentHashMap
是为并发而设计的。它使用分段锁(Java 7 及以前)或 CAS + 节点级锁(Java 8+),允许在不锁定整个 Map 的情况下进行并发的读和写,性能更高。
• Collections.synchronizedMap(map)
是一个同步包装器。它的每一个方法调用(如 put
, get
)都是同步的 (synchronized
),这会导致激烈的线程竞争,性能较低。
示例:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
// 同步包装器
Map syncMap = Collections.synchronizedMap(new HashMap<>());
// 并发 Map
Map concurrentMap = new ConcurrentHashMap<>();
syncMap.put("key", "value");
concurrentMap.put("key", "value");
⚠️ 专业提示:
ConcurrentHashMap
不允许 null
键或 null
值,而 synchronizedMap
(其内部由 HashMap
支持)则允许一个 null
键和多个 null
值。
问题 2:什么是快速失败迭代器 (fail-fast iterator)?它与安全失败迭代器 (fail-safe iterator) 有何不同?
✅ 答案:
• 快速失败迭代器:如果在迭代期间,集合的结构被修改(添加、删除元素等,而非迭代器自己的 remove()
方法),它会立即抛出 ConcurrentModificationException
。ArrayList
和 HashMap
的迭代器都是快速失败的。
• 安全失败迭代器:它在集合的一个副本上进行工作,因此不会抛出异常,但它也无法反映迭代开始后对原始集合所做的修改。
示例(快速失败):
import java.util.ArrayList;
import java.util.List;
List list = new ArrayList<>();
list.add("A");
list.add("B");
for (String item : list) {
if ("B".equals(item)) {
list.remove("B"); // 迭代时修改集合,会抛出 ConcurrentModificationException
}
}
使用 CopyOnWriteArrayList
来获得安全失败的行为:
import java.util.concurrent.CopyOnWriteArrayList;
CopyOnWriteArrayList cowList = new CopyOnWriteArrayList<>();
cowList.add("A");
cowList.add("B");
for (String item : cowList) { // 迭代器在创建时的副本上工作
if ("B".equals(item)) {
cowList.remove("B"); // 修改的是原始集合,不影响迭代器
}
}
// 迭代完成,没有异常
问题 3:为什么 HashMap
不是线程安全的?如果被多个线程访问会发生什么?
✅ 答案:
HashMap
不是线程安全的,因为它没有对并发访问进行任何同步控制。当在没有外部同步的情况下被多个线程同时访问和修改时,可能会发生:
• 数据丢失:一个线程的 put
操作可能被另一个线程的 put
操作覆盖。
• 进入无限循环(在 Java 7 中罕见但致命,由于在扩容期间,并发的 put
操作可能导致内部链表形成环路)。
• 返回不一致的结果:一个线程可能读到只被部分修改的数据。
解决方案:
使用 ConcurrentHashMap
或 Collections.synchronizedMap()
。
问题 4:LinkedHashMap
的内部工作原理是什么?
✅ 答案:
LinkedHashMap
在 HashMap
的哈希桶结构之外,还额外维护了一个双向链表 (doubly linked list)。这个链表将所有条目(entries)连接起来,从而能够保留元素的插入顺序(或者,如果配置了,则保留访问顺序)。
用例(LRU 缓存):
利用其访问顺序特性,可以非常容易地实现一个 LRU (最近最少使用) 缓存:
import java.util.LinkedHashMap;
import java.util.Map;
// 创建一个容量为5的LRU缓存
LinkedHashMap lruCache = new LinkedHashMap<>(16, 0.75f, true) { // 第三个参数 true 表示按访问顺序排序
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 当 size 超过5时,移除最老的条目
return size() > 5;
}
};
lruCache.put("A", "1");
lruCache.put("B", "2");
lruCache.put("C", "3");
lruCache.put("D", "4");
lruCache.put("E", "5");
System.out.println(lruCache.keySet()); // [A, B, C, D, E]
lruCache.get("C"); // 访问 "C"
System.out.println(lruCache.keySet()); // [A, B, D, E, C] (C被移到了末尾)
lruCache.put("F", "6"); // 添加新元素,最老的 "A" 将被移除
System.out.println(lruCache.keySet()); // [B, D, E, C, F]
问题 5:TreeMap
是如何保持排序的?
✅ 答案:
TreeMap
基于一种红黑树 (Red-Black Tree) 的数据结构,它能始终保持所有条目按键 (key) 的自然顺序(或者指定的顺序)进行排序。你也可以在创建 TreeMap
时提供一个自定义的比较器 (Comparator
) 来改变排序规则。
示例:
import java.util.Comparator;
import java.util.TreeMap;
// 使用倒序比较器
TreeMap map = new TreeMap<>(Comparator.reverseOrder());
map.put(1, "A");
map.put(3, "B");
map.put(2, "C");
System.out.println(map); // 输出: {3=B, 2=C, 1=A} (按键倒序)
问题 6:ArrayList
和 LinkedList
的性能差异是什么?
✅ 答案:
操作 |
ArrayList (基于动态数组) |
LinkedList (基于双向链表) |
get(index) 访问 |
O(1) - 极快 |
O(n) - 慢 (需要遍历) |
在末尾 add /remove |
摊销 O(1) - 快 |
O(1) - 快 |
在中间 add /remove |
O(n) - 慢 (需要移动元素) |
O(1) (如果迭代器已在目标位置) |
内存使用 | 更少 |
更多 (每个节点都有额外开销) |
总结: 优先使用 ArrayList
,除非你需要在列表的开头或中间进行大量的插入/删除操作。
问题 7:为什么 HashSet
比 TreeSet
快?
✅ 答案:
• HashSet
由 HashMap
支持,其大部分操作(add
, remove
, contains
)的平均时间复杂度为 O(1)。
• TreeSet
由 TreeMap
(红黑树) 支持,为了维持元素的排序,其操作的时间复杂度为 O(log n)。
因此,只有在保持元素有序是必要需求时才使用 TreeSet
。
问题 8:当两个对象根据 equals()
方法判断相等,但它们的 hashCode()
不等时,会发生什么?
✅ 答案:
这违反了 HashMap
/HashSet
的核心约定。你会得到不一致的行为——可能会出现重复的条目(因为它们可能被哈希到不同的桶中),或者在检索时可能找不到你已经存入的对象(因为 get
操作会根据 hashCode
去找桶,如果 hashCode
不同,可能就找错了地方)。
示例(错误示范):
import java.util.Random;
class BadKey {
@Override
public boolean equals(Object o) { // 总是返回 true
return true;
}
@Override
public int hashCode() { // 每次都返回一个随机数
return new Random().nextInt();
}
}
// HashMap map = new HashMap<>();
// map.put(new BadKey(), "Value1");
// map.put(new BadKey(), "Value2"); // 可能会成功添加,因为 hashCode 不同
// System.out.println(map.size()); // 可能是 2,违反了 Set/Map 的唯一性
千万别这么干。 务必始终同时重写 equals()
和 hashCode()
,并严格遵守它们的约定(相等的对象必须有相同的哈希码)。
问题 9:什么是 WeakHashMap
及其用例?
✅ 答案:
WeakHashMap
的键 (keys) 使用的是弱引用 (weak references)。如果一个键对象在 WeakHashMap
之外没有任何强引用指向它,那么它就可能被垃圾回收器 (GC) 回收。一旦键被回收,它对应的条目就会自动从 Map 中移除。
示例:
import java.util.Map;
import java.util.WeakHashMap;
Map
用例: 非常适合实现缓存、监听器列表或存储与对象相关的元数据。当主对象被回收后,与之关联的缓存/监听器/元数据也应被自动清理,从而避免内存泄漏。
问题 10:PriorityQueue
(优先队列) 和 TreeSet
的区别是什么?
✅ 答案:
• PriorityQueue
允许重复元素,并且它被设计用于高效地检索集合中最小或最大的元素(基于堆 (heap) 数据结构实现),但它不保证除了队首元素之外的其他元素的顺序。
• TreeSet
不允许重复元素,并且它始终维持所有元素的有序状态(基于红黑树),遵循有序集合的语义。
问题 11:CopyOnWriteArrayList
有什么用?
✅ 答案:
它在每一次修改操作(如 add
, remove
, set
)时,都会创建一个底层数组的新副本。它非常适合以下场景:
• 读多写少的并发场景。
• 需要进行安全的迭代而无需加锁或担心 ConcurrentModificationException
。
⚠️ 在写操作密集的工作负载中应避免使用,因为每次写入都会有昂贵的数组复制开销。
问题 12:如何使一个集合不可变?
✅ 答案:
在 Java 9 之前,通常使用 Collections
的包装器方法:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
List mutableList = new ArrayList<>();
mutableList.add("a");
List unmodifiableList = Collections.unmodifiableList(mutableList);
// unmodifiableList.add("b"); // 会抛出 UnsupportedOperationException
从 Java 9+ 开始,使用 List.of()
, Set.of()
, Map.of()
等静态工厂方法更方便:
List list = List.of("a", "b"); // 直接创建不可变列表
// list.add("c"); // 同样会抛出 UnsupportedOperationException
问题 13:EnumMap
是如何工作的?
✅ 答案:
EnumMap
是一个专为枚举类型作为键 (key) 设计的、性能高度优化的 Map 实现。在内部,它使用一个数组来存储值,数组的索引直接由枚举常量的序数 (ordinal()
) 决定。
这种设计提供了 O(1) 的访问性能和极低的内存使用。
示例:
import java.util.EnumMap;
enum Status { ON, OFF }
EnumMap map = new EnumMap<>(Status.class); // 创建时需要传入 key 的 Class 对象
map.put(Status.ON, "正在运行");
map.put(Status.OFF, "已关闭");
问题 14:如何按值 (value) 对 Map
进行排序?
✅ 答案:
import java.util.Map;
import java.util.stream.Collectors;
Map map = Map.of("A", 3, "B", 1, "C", 2);
// 将 entrySet 转换为流,按值排序,然后打印
map.entrySet()
.stream()
.sorted(Map.Entry.comparingByValue()) // 使用 Map.Entry.comparingByValue()
.forEach(e -> System.out.println(e.getKey() + ":" + e.getValue()));
// 输出:
// B:1
// C:2
// A:3
(如果要收集到一个新的、按值排序的 LinkedHashMap
中,可以在 forEach
之后使用 .collect(Collectors.toMap(..., ..., ..., LinkedHashMap::new))
)
问题 15:为什么一些集合方法在接口中是 default
(默认) 方法?
✅ 答案:
从 Java 8 开始,接口可以拥有默认方法 (default
methods)。Java 集合框架利用这个特性来实现向后兼容,并为现有接口(如 List
, Collection
)添加新功能,而不破坏所有已经实现了这些接口的旧代码。
例如,像下面这些在 Java 8 中引入的、非常方便的方法,都是通过默认方法添加的:
• forEach()
• removeIf()
• replaceAll()
• spliterator()