Java 集合框架全景:一张图记住所有接口
Java 集合框架是最常用也最容易用错的部分。它的核心是两个顶层接口:Collection 和 Map。Collection 下面分 List(有序可重复)、Set(无序不重复)、Queue(队列)三个子接口;Map 是键值对结构,独立于 Collection 体系。
记住这个分类就不会选错:需要顺序存东西用 List、需要去重用 Set、需要按键查值用 Map、需要排队用 Queue。每一类下面又有多个实现类,区别在于底层数据结构和性能特性。
选集合先想清楚三个问题:要不要顺序?要不要去重?要不要按键查值?
- ArrayList基于动态数组,随机访问快,中间插入慢。
- HashMap数组加链表加红黑树,JDK 8 后链表长度超 8 转红黑树。
- ConcurrentHashMap线程安全的 Map,分段锁优化,并发场景首选。
- 选型普通场景用 ArrayList 和 HashMap,并发场景换 ConcurrentHashMap。
List 家族:ArrayList vs LinkedList
List 有两个主要实现:ArrayList 和 LinkedList。面试常问区别,实际开发中 95% 的情况都用 ArrayList。
| 特性 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 get(i) | O(1) 快 | O(n) 慢 |
| 尾部添加 | 均摊 O(1) | O(1) |
| 中间插入删除 | O(n) 要搬移元素 | O(1) 改指针 |
| 内存占用 | 连续数组,紧凑 | 每个节点多两个指针 |
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add(1, "王五"); // 在索引 1 插入
// 遍历
for (String name : names) {
System.out.println(name);
}
// Java 8 流式操作
names.stream()
.filter(n -> n.startsWith("张"))
.forEach(System.out::println);
什么时候用 LinkedList?当你需要频繁在头部插入删除(比如做队列),或者数据量很大且频繁中间插入。但实际项目中这种场景极少,大多数时候用 ArrayList 就够了。
Set 家族:HashSet 与 TreeSet 的区别
Set 最核心的特性是不允许重复元素。最常用的是 HashSet,底层就是一个 HashMap(value 是固定占位对象)。需要排序时用 TreeSet,底层是红黑树。
Set<String> tags = new HashSet<>();
tags.add("Java");
tags.add("Python");
tags.add("Java"); // 重复,不会加入
System.out.println(tags.size()); // 2
// 去重场景:列表转 Set 去重
List<String> raw = List.of("a", "b", "a", "c", "b");
Set<String> unique = new HashSet<>(raw);
System.out.println(unique); // [a, b, c]
// TreeSet 自动排序
Set<Integer> nums = new TreeSet<>();
nums.add(30);
nums.add(10);
nums.add(20);
System.out.println(nums); // [10, 20, 30]
判断重复的依据是 equals 和 hashCode。自定义对象放进 HashSet 时,必须同时重写 equals 和 hashCode,否则会出现"看起来一样但被当成不同对象"的 bug。
Map 家族:HashMap 的底层原理
HashMap 是 Java 里用得最多的集合。理解它的底层原理能帮你写出更高效的代码。JDK 8 的 HashMap 底层是数组加链表加红黑树:
- put 一个键值对时,先对 key 算 hash,映射到数组下标。
- 那个位置如果是空的,直接放进去。
- 如果已经有元素(哈希冲突),就把新元素接到链表尾部。
- 当链表长度超过 8 且数组容量大于 64 时,链表转成红黑树,查找从 O(n) 降到 O(log n)。
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 90);
scores.put("李四", 85);
scores.putIfAbsent("王五", 100); // 不存在才放
// 遍历键值对
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Java 8 新增方法
scores.getOrDefault("赵六", 0); // 不存在返回默认值
scores.replace("张三", 95); // 替换
scores.computeIfAbsent("钱七", k -> k.length()); // 不存在时计算值
性能要点:HashMap 默认初始容量是 16,负载因子 0.75。如果预知数据量大,创建时指定初始容量避免频繁扩容:new HashMap<>(128)。
并发容器:ConcurrentHashMap 怎么用
HashMap 不是线程安全的。多线程同时 put 可能导致数据丢失甚至死循环(JDK 7 的头插法扩容问题)。并发场景下有两个选择:Collections.synchronizedMap(全局锁,性能差)或 ConcurrentHashMap(分段锁,性能好)。
// 线程安全的 Map
Map<String, Integer> cache = new ConcurrentHashMap<>();
// 多线程环境下安全写入
cache.put("key1", 100);
cache.putIfAbsent("key2", 200);
// 复合操作也安全:先查再写
cache.compute("key1", (k, v) -> v == null ? 1 : v + 1);
// Java 8 后 ConcurrentHashMap 支持流式操作
cache.forEach((k, v) -> System.out.println(k + "=" + v));
JDK 8 的 ConcurrentHashMap 放弃了分段锁,改用 CAS 加 synchronized 锁单个桶节点,并发粒度更细,性能更好。实际开发中,只要是多线程共享的 Map,一律用 ConcurrentHashMap,不要用 HashMap。
| 容器 | 适用场景 |
|---|---|
| HashMap | 单线程或局部变量 |
| ConcurrentHashMap | 多线程共享的缓存、配置 |
| CopyOnWriteArrayList | 读多写少的列表(如监听器列表) |
| BlockingQueue | 生产者消费者模式 |
队列与栈:ArrayDeque 与 PriorityQueue
队列在并发编程和算法里都很常用。Java 里最实用的队列实现是 ArrayDeque(数组双端队列)和 PriorityQueue(优先队列)。
// ArrayDeque 做栈和队列
Deque<String> stack = new ArrayDeque<>();
stack.push("第一");
stack.push("第二");
System.out.println(stack.pop()); // 第二(后进先出)
Deque<String> queue = new ArrayDeque<>();
queue.offer("任务1");
queue.offer("任务2");
System.out.println(queue.poll()); // 任务1(先进先出)
// PriorityQueue 自动排序
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.offer(30);
pq.offer(10);
pq.offer(20);
System.out.println(pq.poll()); // 10(最小的先出)
ArrayDeque 比 Stack 类和 LinkedList 做队列都更快——Stack 继承了 Vector(全局锁),LinkedList 有节点指针开销。PriorityQueue 适合做任务调度,优先级高的任务先执行。
选型速查表:什么时候用哪个
实际开发中,大多数选择可以用这张速查表解决:
- 存一组有序数据,需要按下标访问:ArrayList。
- 存一组数据,需要自动去重:HashSet。
- 按键查值,单线程:HashMap。
- 按键查值,多线程共享:ConcurrentHashMap。
- 需要自动排序的集合:TreeSet 或 TreeMap。
- 做栈或队列:ArrayDeque。
- 优先级任务调度:PriorityQueue。
集合选型没有银弹,但记住默认用 ArrayList 和 HashMap,并发场景换 ConcurrentHashMap,遇到特殊需求再查文档——这覆盖了 90% 的场景。