Java 中的集合类有哪些?如何分类的?
结论先行:集合框架分成两大阵营——Collection(单个元素的集合)与 Map(键值对映射),Map 不继承 Collection。Collection 下再按语义分三支:List 有序可重复、Set 不重复、Queue / Deque 按队列规则进出。按线程安全又能分成基础实现、java.util.concurrent 并发容器、以及 List.of 这类不可变集合。
1. 一张图记住体系
Iterable
└── Collection
├── List(有序、可重复、有下标)
│ ├── ArrayList 数组,随机访问快
│ ├── LinkedList 双向链表,兼 Deque
│ ├── Vector 遗留类,方法级同步
│ └── CopyOnWriteArrayList 写时复制,读多写少
├── Set(不重复)
│ ├── HashSet 哈希,无序
│ ├── LinkedHashSet 插入顺序
│ ├── TreeSet 排序(红黑树)
│ ├── CopyOnWriteArraySet
│ └── ConcurrentSkipListSet
└── Queue / Deque(队列)
├── ArrayDeque 双端队列,可当栈
├── PriorityQueue 小顶堆
├── ArrayBlockingQueue / LinkedBlockingQueue
├── SynchronousQueue / DelayQueue
└── ConcurrentLinkedQueue 无锁并发队列
Map(独立接口,不继承 Collection)
├── HashMap 数组 + 链表 + 红黑树
├── LinkedHashMap 额外维护插入/访问顺序
├── TreeMap 红黑树,按 key 排序
├── Hashtable 遗留类,整表锁
├── ConcurrentHashMap CAS + 桶级锁
├── ConcurrentSkipListMap 跳表,有序并发
├── WeakHashMap 弱引用 key,可被 GC
├── IdentityHashMap 用 == 比较 key
└── EnumMap 枚举 key,数组实现
2. 三个子接口的语义边界
| 接口 | 有序性 | 允许重复 | 允许 null | 代表实现 | 并发替代 |
|---|---|---|---|---|---|
List | 有,按插入位置 | 是 | 是 | ArrayList | CopyOnWriteArrayList |
Set | 取决于实现 | 否 | HashSet 允许 1 个 | HashSet | ConcurrentHashMap.newKeySet() |
Queue | 有,按入队顺序 | 是 | LinkedList 允许 | ArrayDeque | LinkedBlockingQueue |
Deque | 有,两端可操作 | 是 | ArrayDeque 不允许 | ArrayDeque | LinkedBlockingDeque |
Map | 取决于实现 | key 不重复 | HashMap 允许 | HashMap | ConcurrentHashMap |
3. Map 的选型看“顺序”和“并发”
| 实现 | 顺序 | 底层 | 适用场景 |
|---|---|---|---|
| HashMap | 无序 | 数组 + 链表 + 红黑树 | 通用键值存储 |
| LinkedHashMap | 插入顺序或访问顺序 | HashMap + 双向链表 | LRU 缓存、保持输出顺序 |
| TreeMap | key 排序 | 红黑树 | 范围查询、排行榜 |
| ConcurrentHashMap | 无序 | CAS + 桶级锁 | 并发读写 |
| ConcurrentSkipListMap | key 排序 | 跳表 | 高并发有序场景 |
| WeakHashMap | 无序 | 哈希 + 弱引用 | 缓存、监听器注册表 |
4. 代码:按场景选容器
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class CollectionPick {
public static void main(String[] args) {
// 去重且保留首次出现顺序
Set<String> seen = new LinkedHashSet<>();
seen.add("b");
seen.add("a");
seen.add("b");
System.out.println(seen); // 输出:[b, a]
// 需要按 key 排序或范围查询
Map<Integer, String> sorted = new TreeMap<>();
sorted.put(3, "c");
sorted.put(1, "a");
System.out.println(sorted.firstKey()); // 输出:1
// 栈语义用 ArrayDeque,而不是遗留的 Stack
Deque<Integer> stack = new ArrayDeque<>();
stack.push(1);
stack.push(2);
System.out.println(stack.pop()); // 输出:2
// 不可变集合:共享安全,修改直接抛异常
List<String> ro = List.of("a", "b");
System.out.println(ro.size()); // 输出:2
}
}
5. 遗留类与常见坑
Vector、Hashtable、Stack都是 JDK 1.0 的遗留实现。Stack继承Vector,每个方法都带synchronized,已被ArrayDeque取代。Arrays.asList(array)返回定长列表,底层就是原数组,add/remove会抛UnsupportedOperationException。List.of返回真正的不可变集合,不接受 null,重复元素直接抛IllegalArgumentException。Collections.unmodifiableList是视图,原列表改动仍会反映出来。- 并发容器各有适用面:
CopyOnWriteArrayList适合读多写少,ConcurrentLinkedQueue适合无界高并发队列,BlockingQueue家族用于生产者-消费者。
常见追问
- 为什么 Map 不继承 Collection? 语义不同:Collection 是元素容器,Map 是键到值的映射。若强行继承,
add与put的签名和语义会冲突,keySet、values这些视图也难自洽。 - Collection 和 Collections 有什么区别? 前者是接口,后者是工具类,提供排序、同步包装、不可变包装等静态方法。
- RandomAccess 有什么用? 空标记接口,告诉通用算法“下标访问是 O(1)”,
Collections.binarySearch据此选择遍历方式。 - 为什么 ConcurrentHashMap 不允许 null? 并发下无法区分“没有这个 key”和“值就是 null”。
- 如何快速选容器? 先问三个问题:要不要 key、要不要去重、要不要顺序;再看是否并发。
- HashMap 和 Hashtable 的初始容量为什么不同? 前者容量取 2 的幂用位运算定位,后者取质数用取模散列。
小结
集合框架的记忆主线是“两个顶层接口 + 三种语义 + 一条并发支线”:Collection 管单元素,Map 管键值对;List 管顺序、Set 管唯一、Queue 管进出规则;并发场景从 java.util.concurrent 里挑对应实现。选型时先确定语义,再看顺序与并发要求,最后才比较常数性能。