Java 集合框架的整体结构是怎样的?
简化版
Java 集合分两大体系:Collection(单个元素的集合)和 Map(键值对)。Collection 下面三支——List(有序可重复)、Set(无序不重复)、Queue(队列)。常用实现:List 有 ArrayList/LinkedList,Set 有 HashSet/TreeSet,Map 有 HashMap/TreeMap,Queue 有 ArrayDeque。选型看你要有序还是去重、要不要排序、并发不并发。
详细版
整体结构:
Iterable
└─ Collection
├─ List (有序、可重复、有索引)
│ ├─ ArrayList 动态数组,随机访问快
│ └─ LinkedList 双向链表,也实现了 Deque
├─ Set (不重复)
│ ├─ HashSet 无序,基于 HashMap
│ ├─ LinkedHashSet 保持插入顺序
│ └─ TreeSet 自动排序,基于红黑树
└─ Queue (队列/双端队列)
├─ ArrayDeque 数组实现的双端队列(做栈/队列都推荐)
└─ PriorityQueue 优先级队列(堆)
Map (键值对,不属于 Collection)
├─ HashMap 无序,最常用
├─ LinkedHashMap 保持插入/访问顺序(可做 LRU)
├─ TreeMap 按 key 排序,红黑树
└─ Hashtable 遗留的线程安全 Map(已淘汰)
三大接口特征:
- List:有序(按插入顺序)、可重复、有下标。要「一串按顺序、可重复的元素」用它。
- Set:不可重复。要「去重」用它,靠
hashCode+equals判重。 - Map:key→value 映射,key 不重复。要「按 key 查 value」用它。Map 不是 Collection 的子接口,它是独立体系。
完整版教学
一、先建立两条主线:Collection 和 Map
集合框架看着庞大,其实就两棵树:
- Collection(继承自
Iterable,所以能 for-each):装单个元素,下分 List / Set / Queue。 - Map:装键值对,独立于 Collection。
初学者常问「Map 为什么不在 Collection 里」——因为 Collection 的抽象是「一组元素」,而 Map 是「一组映射关系」,元素形态不同(一个是 element,一个是 key-value 对),强行归一反而别扭,所以 JDK 让它独立成体系。
二、List / Set / Queue 各自解决什么
- List——要顺序、要重复、要按位置访问:日志列表、购物车、查询结果。首选
ArrayList(随机访问快、缓存友好),频繁两端操作用ArrayDeque。 - Set——要去重:标签、参与用户 id、去重统计。
HashSet最快(无序),要保持插入序用LinkedHashSet,要排序用TreeSet。 - Queue/Deque——要先进先出或双端操作:任务队列、BFS。用
ArrayDeque(做栈和队列都比Stack/LinkedList好),要按优先级出队用PriorityQueue。
记忆点:有序可重复选 List,去重选 Set,排队选 Queue,键值映射选 Map。
三、Hash / Linked / Tree 三个前缀的含义
集合类名前缀是有规律的,认准就能秒懂特性:
- Hash 前缀(HashMap/HashSet):基于哈希表,无序、增删查 O(1)、最快。
- Linked 前缀(LinkedHashMap/LinkedHashSet):哈希表 + 链表,保持插入/访问顺序。
- Tree 前缀(TreeMap/TreeSet):基于红黑树,自动排序、增删查 O(log n)、能做范围查询(如
subMap、headSet)。
需求映射:不在乎顺序求最快 → Hash;要保留插入顺序 → Linked;要排序或范围查 → Tree。
四、线程安全怎么办
上面这些默认都不是线程安全的。并发场景有几种做法:
- 首选并发容器:
ConcurrentHashMap(并发 Map)、CopyOnWriteArrayList(读多写少的 List)、ConcurrentLinkedQueue; - 遗留同步类:
Vector、Hashtable—— 全表锁,性能差,不推荐; - 包装同步:
Collections.synchronizedList/Map(...)—— 也是全表锁,一般不如并发容器。
五、接口、实现与视图要分清
变量尽量声明为接口,例如 List<String> names = new ArrayList<>(),让调用方依赖 List 契约而不是数组实现。这样以后换成不可变列表或并发列表时,使用方不必跟着改;但若代码确实需要 ensureCapacity 这类实现专属能力,就应显式承认自己依赖 ArrayList。
集合 API 还经常返回“视图”而不是独立副本。map.keySet()、map.values() 和 map.entrySet() 与原 Map 相连,从视图删除元素会改变原 Map;list.subList(from, to) 也共享底层结构。若需要隔离快照,应再构造一份新集合。
Map<String, Integer> scores = new HashMap<>();
scores.put("A", 90);
Set<String> keys = scores.keySet();
keys.remove("A"); // scores 也删除了 A
List<Integer> snapshot = new ArrayList<>(source); // 独立容器快照
| API 结果 | 是否独立 | 修改影响 |
|---|---|---|
map.keySet() | 否,Map 视图 | 删除会反映到 Map |
list.subList(a,b) | 否,List 视图 | 结构修改可能互相影响 |
Arrays.asList(array) | 固定大小视图 | 可 set,不能 add/remove,元素与数组联动 |
new ArrayList<>(source) | 新容器 | 容器结构独立,元素对象仍共享 |
List.copyOf(source) | 不可变副本/复用 | 不允许修改,且拒绝 null 元素 |
六、常见误区与追问
- 误区:Map 属于 Collection。 它是独立的键值映射体系,只能通过 keySet、values、entrySet 暴露集合视图。
- 误区:Hash 前缀意味着遍历顺序固定。 哈希实现不承诺顺序,顺序需求应选择 Linked 或 Tree 实现。
- 误区:
Arrays.asList是普通 ArrayList。 它是数组支持的固定大小列表,增删会抛UnsupportedOperationException。 - 追问:List 的“有序”是否等于“自动排序”? 不是,有序指保留位置/迭代顺序;自动按比较规则排序的是 TreeSet、TreeMap 等。
- 追问:Queue 的
add/remove/element与offer/poll/peek有何区别? 前一组失败时抛异常,后一组用返回值表示容量不足或队列为空。 - 追问:普通同步包装是否等于复合操作安全? 不是,遍历或“先检查再修改”仍需按文档在同一锁上同步。
选型心法:先定语义接口,再定顺序、复杂度和并发实现,最后确认拿到的是独立集合还是共享视图。
泛型约束也属于集合契约。List<? extends Number> 适合读取 Number,却不能安全添加具体数字;List<? super Integer> 可以写入 Integer,读取时只能按 Object 看待,这就是常说的 PECS:生产者 extends,消费者 super。
static double sum(List<? extends Number> values) { /* 只读 */ }
static void addDefaults(List<? super Integer> out) { out.add(0); }
它解决的是不同泛型集合之间的类型协作,不会改变底层容器的顺序、复杂度或线程安全特征。
七、加强记忆
集合框架先分 Collection 与 Map 两条主线;Collection 再按 List 的位置语义、Set 的唯一性和 Queue 的排队语义选择。实现名前缀提示底层取舍:Hash 求平均 O(1),Linked 保持顺序,Tree 提供排序与范围查询。面向接口声明变量,并发时选专用容器,还要警惕 keySet、subList、Arrays.asList 等共享视图,才能从“会背类名”进阶到正确使用集合契约。