Java集合面试“送命题”合集!这15个问题,你能答对几个?

问题 1:ConcurrentHashMap 和 Collections.synchronizedMap() 有什么区别?

✅ 答案:

两者都提供线程安全的 Map,但实现方式截然不同

  • • ConcurrentHashMap 是为并发而设计的。它使用分段锁(Java 7 及以前)或 CAS + 节点级锁(Java 8+),允许在不锁定整个 Map 的情况下进行并发的读和写,性能更高。

  • • Collections.synchronizedMap(map) 是一个同步包装器。它的每一个方法调用(如 putget)都是同步的 (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() 方法),它会立即抛出 ConcurrentModificationExceptionArrayList 和 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 支持,其大部分操作(addremovecontains)的平均时间复杂度为 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 map = new WeakHashMap<>();
Object key = new Object(); // 这是一个强引用
map.put(key, "value");
System.out.println(map.size()); // 1

key = null; // 移除了唯一的强引用,现在 key 对象只被 WeakHashMap 弱引用
System.gc(); // 建议 JVM 进行垃圾回收 (不保证立即执行)
// 在某个时间点后,再次检查 map 的大小,它很可能变为 0
// System.out.println(map.size());

用例: 非常适合实现缓存监听器列表或存储与对象相关的元数据。当主对象被回收后,与之关联的缓存/监听器/元数据也应被自动清理,从而避免内存泄漏。


问题 10:PriorityQueue (优先队列) 和 TreeSet 的区别是什么?

✅ 答案:

  • • PriorityQueue 允许重复元素,并且它被设计用于高效地检索集合中最小或最大的元素(基于堆 (heap) 数据结构实现),但它不保证除了队首元素之外的其他元素的顺序。

  • • TreeSet 不允许重复元素,并且它始终维持所有元素的有序状态(基于红黑树),遵循有序集合的语义。


问题 11:CopyOnWriteArrayList 有什么用?

✅ 答案:

它在每一次修改操作(如 addremoveset)时,都会创建一个底层数组的新副本。它非常适合以下场景:

  • • 读多写少的并发场景。

  • • 需要进行安全的迭代无需加锁或担心 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 集合框架利用这个特性来实现向后兼容,并为现有接口(如 ListCollection)添加新功能,而不破坏所有已经实现了这些接口的旧代码。

例如,像下面这些在 Java 8 中引入的、非常方便的方法,都是通过默认方法添加的:

  • • forEach()

  • • removeIf()

  • • replaceAll()

  • • spliterator()

你可能感兴趣的:(java,面试,python)