引言
在Java程序中,存储和操作一组对象是最常见的需求。然而,在Java诞生的早期,这项看似简单的任务却充满了挑战。开发者只能依赖数组、Vector、Hashtable这几个零散的类,它们各自为政,没有统一的接口,使用起来既不方便也不统一。
这段"黑暗历史"的终结,始于1998年Java 2平台(JDK 1.2)中一个重量级新成员的加入------Java集合框架(Java Collections Framework, JCF)。由Joshua Bloch主导设计的这个框架,不仅统一了数据结构的API,更深刻影响了无数Java开发者的编程习惯和思维方式。
此后二十余年间,集合框架伴随着Java语言的演进,经历了泛型、并发集合、函数式编程、有序集合统一等一次又一次的重大革新。今天,我们顺着时间线,用最详尽的笔触,还原Java集合框架从无到有、从特设到通用、从命令式到函数式的完整发展历程。
第一章:前集合框架时代------混沌与探索(JDK 1.0 - 1.1)
1.1 早期数据结构的困境(1996-1997)
当Java在1996年随着JDK 1.0问世时,核心类库中能够用于存储对象群组的工具屈指可数,且设计简陋:
| 数据结构 | 引入版本 | 特点与局限 |
|---|---|---|
| 数组(Array) | JDK 1.0 | 长度固定,一旦创建便不能改变;只能存储同一类型的元素;功能原始 |
| Vector | JDK 1.0 | 一个可动态增长的"数组"类,但所有方法都是同步的(线程安全),在单线程环境下有不必要的性能开销 |
| Hashtable | JDK 1.0 | 一个用于存储键值对的类,同样所有方法都是同步的,且不允许null键和值 |
| Enumeration | JDK 1.0 | 用于遍历Vector和Hashtable的唯一接口,功能单一,不能进行结构性修改 |
| BitSet | JDK 1.0 | 一个可以动态增长的位向量类,用于高效存储布尔值 |
| Stack | JDK 1.0 | 继承自Vector,实现了后进先出(LIFO)的栈数据结构 |
这些早期的类被统称为"特设类"(Ad-hoc Classes)。它们最大的问题在于缺乏一个统一的设计:
-
没有共同的接口 :
Vector使用elementAt方法,Hashtable使用get和put方法,数组使用方括号([]),你无法用一种统一的方式去操作它们。 -
难以扩展:很难在这些类的基础上构建新的、通用的数据结构。
-
不支持集合操作:没有现成的方法来对一组元素进行排序、查找、取交集等常见操作。
1.2 来自社区的探索:Doug Lea的Collections与ObjectSpace的JGL
官方能力的缺失激发了社区的创造力。在JDK 1.2发布前,涌现了两个非常有影响力的第三方集合库,它们直接影响了后来集合框架的设计。
1. Doug Lea的Collections包(1995年秋)
著名的并发专家Doug Lea(当时在纽约州立大学奥斯威戈分校任教)在1995年秋就发布了一套公共领域的集合类。这被认为是第一个被广泛使用的Java集合库,包含了多种数据结构的实现,为后续发展奠定了基础。
2. ObjectSpace的JGL(1996年6月)
很多从C++转战Java的开发者,对标准模板库(STL)的强大功能念念不忘。ObjectSpace公司敏锐地捕捉到这个需求,在1996年6月发布了Java Generic Library(JGL)。
JGL的目标非常明确:将C++ STL的设计哲学和功能移植到Java中。它提供了大约130个类和接口,远超后来官方的集合框架(约为其5倍大小),拥有50多种算法。JGL一度成为了事实上的标准,获得了10多家IDE厂商的支持,宣称拥有超过10万用户。
然而,JGL有两个主要问题:
-
名字问题:Sun的法律部门不喜欢"Generic"这个词(当时Java还没有泛型),迫使ObjectSpace改名,但JGL的缩写保留了下来。
-
体积庞大:对于Sun而言,将如此庞大、模仿C++的库作为Java的标准库,违背了他们希望保持Java核心简洁的理念。
1.3 催生官方标准的契机
JDK 1.0和1.1中特设类的混乱,以及社区中JGL的流行,给Sun带来了巨大的压力。他们认识到,Java迫切需要一套标准的、统一的、易于使用和扩展的容器类库。这不仅是为了简化开发者的工作,更是为了让Java平台自身更加成熟和稳健。
于是,一项代号为JSR-000166的Java规范请求被发起,目标是设计一个"用于操作对象集合的框架"。这个任务落到了当时在Sun工作的Joshua Bloch身上。
第二章:集合框架的诞生------JDK 1.2(1998年12月)
1998年12月4日 ,随着Java 2平台(J2SE 1.2)的发布,万众期待的Java集合框架正式成为Java标准库的一部分。
2.1 核心设计者与设计理念
Java集合框架主要由Joshua Bloch设计开发。他后来在《Effective Java》一书中详细阐述了关于API设计的诸多洞见,这些思想深深植根于集合框架的设计之中。
Joshua Bloch为集合框架确立了三个核心设计目标:
-
高性能:对基本数据结构(如动态数组、链表、哈希表、树)的实现必须高效。
-
高互操作性:不同类型的集合应有统一的操作方式,并允许高度互操作。
-
易于扩展:框架本身应该是可扩展的,允许开发者在此基础上构建更专门化的实现。
2.2 核心架构:接口、实现与算法
集合框架的架构完美地体现了"面向接口编程"的原则,主要由三部分构成:
1. 核心接口:定义了集合的抽象数据类型。
-
Collection:所有集合的根接口,定义了add()、remove()、size()、iterator()等基本操作。 -
List:继承自Collection,代表有序、可重复的元素集合,可以通过索引访问。 -
Set:继承自Collection,代表不可重复的元素集合,类似于数学中的集合概念。 -
SortedSet:继承自Set,代表元素自动排序的集合。 -
Map:一个独立的根接口,代表键值对(Key-Value)的映射。 -
SortedMap:继承自Map,代表键自动排序的映射。
2. 通用实现:为核心接口提供了可复用的实现类。
-
List的实现 :ArrayList(基于动态数组,随机访问快)、LinkedList(基于双向链表,插入删除快)。 -
Set的实现 :HashSet(基于哈希表,查找快)、TreeSet(基于红黑树,元素有序,实现SortedSet)。 -
Map的实现 :HashMap(基于哈希表)、TreeMap(基于红黑树,键有序,实现SortedMap)。
3. 算法 :在java.util.Collections工具类中提供了大量用于操作集合的静态方法,如sort()、binarySearch()、reverse()、shuffle()等。
2.3 工具类的引入
JDK 1.2还引入了java.util.Arrays工具类,它包含了一组可用于数组的静态方法:
-
equals():比较两个数组是否相等 -
fill():用指定的值填充数组 -
sort():对数组进行排序 -
binarySearch():在已排序的数组中查找元素 -
asList():将数组转换为List容器
2.4 革命性意义
集合框架的诞生,对于Java开发者而言是一场彻底的解放:
-
降低编程难度:开发者无需再自己实现链表、哈希表等基础数据结构,可以直接使用标准、可靠的类。
-
提升代码质量 :统一的接口让代码更清晰,易于理解和维护。
Collections工具类提供的算法经过优化,性能有保障。 -
增加代码重用性 :针对
List或Collection接口编写的代码,可以应用于任何实现了该接口的类。
2.5 与遗留类的兼容
为了照顾已有的大量代码,JDK 1.2对Vector和Hashtable这些遗留类进行了改造,让它们"融入"了新的框架:
-
Vector被改造为实现了List接口。 -
Hashtable被改造为实现了Map接口。
然而,官方强烈建议新代码不要再使用这些遗留类,而是使用它们的非同步替代品:
-
用
ArrayList替代Vector。 -
用
HashMap替代Hashtable。 -
用
ArrayDeque替代Stack。
第三章:稳步发展与扩充(JDK 1.3 - 1.4)
在JDK 1.2之后,集合框架进入了稳步完善和功能扩充的阶段。
3.1 JDK 1.3(2000年)
JDK 1.3的主要贡献在于巩固和完善:
-
TreeSet和TreeMap的正式实现 :虽然SortedSet和SortedMap接口在1.2就已存在,但基于红黑树的标准实现TreeSet和TreeMap在1.3中变得更加成熟和稳定。 -
Iterator接口的普及 :虽然迭代器的概念在1.2就已引入,但Iterator完全取代了老旧的Enumeration,成为遍历集合的首选方式。 -
Collections工具类 :正式引入并完善了Collections工具类,提供排序、查找、同步包装等方法。
3.2 JDK 1.4(2002年)
JDK 1.4带来了几个重要的新实现,增强了集合框架的功能性和灵活性:
-
LinkedHashSet和LinkedHashMap:这是非常重要的增强。它们继承自HashSet和HashMap,但在内部维护了一个双向链表,记录了元素插入的顺序。这让你在拥有哈希表高效查找的同时,还能按照插入顺序进行遍历(预测性迭代)。 -
IdentityHashMap:这个特殊的Map实现使用引用相等性(==)而不是对象相等性(equals())来比较键。它在某些特定场景(如序列化、代理)中有用武之地。 -
WeakHashMap:这个Map的实现使用"弱引用"(WeakReference)来持有键。当一个键对象不再被外部引用时,它会被垃圾回收,其对应的键值对也会自动从WeakHashMap中移除。这对于实现缓存等场景非常有用。
3.3 同期生态发展:Jakarta Commons Collections(2001)
2001年7月,Jakarta Commons Collections组件发布,它用特制的数据类型和新方法扩展了J2SE 1.2的API。它提供了FastArrayList、Bag、PriorityQueue、LRUMap等特殊实现,丰富了Java集合生态。
第四章:并发集合的诞生------JSR 166与Doug Lea的回归(JDK 5)
4.1 技术背景
随着多核处理器的普及,并发编程变得越来越重要。原有的集合类(如Vector、Hashtable)采用简单的全同步策略,性能低下;而新生的非同步集合(如ArrayList、HashMap)又不能在多线程环境下安全使用。开发者急需一套高性能、线程安全的并发集合。
4.2 JSR 166的成立与Doug Lea的贡献
JSR 166 (Java Specification Request 166)于2001年1月成立,由Doug Lea领导。该组织的目标是将他此前开发的util.concurrent类库中的诸多高水平概念并入Java核心类库。经过多年的努力,这份工作在JDK 5中开花结果。
4.3 java.util.concurrent包的诞生
JDK 5 中,java.util.concurrent包正式成为Java标准库的一部分。这个包中包含了大量高性能、线程安全的集合类:
| 并发集合类 | 引入版本 | 描述 |
|---|---|---|
ConcurrentHashMap |
JDK 5 | 一个高性能、线程安全的HashMap实现。它使用细粒度的锁机制(JDK 7采用锁分段,JDK 8采用CAS+synchronized),允许多个线程并发读写,极大地提升了并发环境下的吞吐量。 |
CopyOnWriteArrayList |
JDK 5 | 一个线程安全的List变体。所有修改操作(add、set等)都会创建底层数组的一个新副本。适用于读操作远多于写操作的场景,如监听器列表。 |
CopyOnWriteArraySet |
JDK 5 | 基于CopyOnWriteArrayList实现的线程安全Set。 |
ConcurrentLinkedQueue |
JDK 5 | 一个基于链表、无界的、非阻塞的、线程安全的队列,使用高效的CAS操作实现。 |
**BlockingQueue**接口 |
JDK 5 | 支持阻塞操作的队列接口。 |
ArrayBlockingQueue |
JDK 5 | 基于定长数组的阻塞队列实现,有界,线程安全。 |
LinkedBlockingQueue |
JDK 5 | 基于单向链表的阻塞队列实现,无界但可选容量界限,线程安全。 |
PriorityBlockingQueue |
JDK 5 | 基于优先堆的阻塞队列实现,线程安全。 |
SynchronousQueue |
JDK 5 | 一种线程安全无缓冲的无界阻塞队列,每个put必须等待一个take,反之亦然。 |
DelayQueue |
JDK 5 | 一种有序无界阻塞队列,只有在延迟期满时才能从中提取元素,线程安全。 |
4.4 Queue接口的正式引入
JDK 5还正式引入了Queue接口,定义了队列数据结构,大多遵循先进先出(FIFO)的模式。PriorityQueue作为其非线程安全实现也被引入,它基于优先堆实现,元素按自然顺序或Comparator排序。
4.5 泛型革命(JDK 5,2004年)
2004年9月30日 ,JDK 5正式发布。这是Java语言历史上最重要的版本之一,而它带来的最核心特性之一------泛型,对集合框架进行了一次脱胎换骨的重塑。
泛型之前的集合:类型不安全:
java
List list = new ArrayList();
list.add("Hello");
list.add(123); // 编译通过!运行时才发现问题
String s = (String) list.get(0); // 必须强转
泛型带来的改变:
java
List<String> list = new ArrayList<String>();
list.add("Hello");
// list.add(123); // 编译错误!类型安全
String s = list.get(0); // 无需强转
泛型带来的核心好处:
-
编译时类型安全:编译器现在可以检查放入集合的对象类型是否正确,将潜在的错误扼杀在摇篮中。
-
消除强制类型转换:代码变得更简洁、更清晰。
-
更强的抽象能力:开发者可以编写出能够操作多种类型集合的泛型算法。
4.6 EnumSet:高效处理枚举类型
JDK 5还引入了EnumSet类,它是一种特殊的Set实现,用于高效地存储枚举类型的元素。EnumSet完全基于位向量实现,极其紧凑和高效。EnumSet是抽象类,有两种实现方式RegularEnumSet和JumboEnumSet,但这些都是包私有的,必须通过工厂方法(如EnumSet.of())来创建实例。
第五章:持续优化与增强(JDK 6 - 7)
5.1 JDK 6(2006年)
JDK 6引入了一些新接口,进一步完善了集合框架的体系:
-
NavigableSet接口 :继承自SortedSet,提供了导航方法如floor()、ceiling()、lower()、higher()。 -
NavigableMap接口 :继承自SortedMap,提供了类似的导航方法。 -
Deque接口 :继承自Queue,代表双端队列,支持在两端插入和移除元素。 -
ArrayDeque类 :Deque接口的实现,基于循环数组,无限扩展且可选容量,比Stack和LinkedList更适合作为栈或队列使用。
TreeSet和TreeMap分别在JDK 6中被改造为实现了NavigableSet和NavigableMap接口。
5.2 JDK 7(2011年)
JDK 7引入了TransferQueue接口和LinkedTransferQueue实现:
-
TransferQueue:继承自BlockingQueue,增加了生产者等待消费者消费的语义。 -
LinkedTransferQueue:TransferQueue的无界实现。
第六章:函数式风格的渗透与HashMap革命------JDK 8(2014年)
2014年3月18日,JDK 8发布。Lambda表达式和Stream API的加入,是集合框架自泛型以来最重要的一次演进。
6.1 接口中的默认方法(Default Methods)
为了在不破坏现有实现类的前提下将Stream API无缝集成到集合框架中,Java 8引入了接口的默认方法 。例如,Collection接口新增了stream()和parallelStream()默认方法,所有继承自Collection的类(如ArrayList、HashSet等)都自动获得了这两个方法。
此外,Iterable接口也新增了forEach()默认方法,使得遍历集合变得更加简洁。
6.2 Stream API:数据处理的革命
Stream API是JDK 8中最激动人心的新功能。它允许你以声明式的方式(类似于编写SQL语句)对集合数据进行复杂的操作,如过滤、映射、查找、聚合等。
java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
// 获取所有长度大于3的名字,转为小写,并排序
List<String> result = names.stream()
.filter(name -> name.length() > 3) // 中间操作
.map(String::toLowerCase) // 中间操作
.sorted() // 中间操作
.collect(Collectors.toList()); // 终端操作
Stream API的优势:
-
声明式编程:你告诉代码"做什么",而不是"怎么做",代码更简洁、更易读。
-
组合性:可以将多个操作流畅地组合在一起。
-
并行能力 :只需将
stream()换成parallelStream(),就可以轻松地将操作并行化,利用多核处理器的能力。
JDK 8还引入了Spliterator接口,这是一种可并行遍历的迭代器,用于支持Stream API的并行处理。
6.3 HashMap的红黑树化------JDK 8的重大性能优化
JDK 8对HashMap的实现进行了重大优化,解决了哈希碰撞攻击的潜在风险。
在JDK 7及之前,HashMap的底层结构是数组+链表。当大量键的哈希值相同时,它们会被放入同一个桶中,形成一个长链表,查找性能会退化到 O(n)。
JDK 8的改进是引入了**"链表转红黑树"**的机制:
-
当一个桶中的元素个数超过阈值(默认为
TREEIFY_THRESHOLD = 8)且数组长度大于64时,HashMap会将这个桶的存储结构从链表转换为红黑树。 -
红黑树的查找、插入和删除性能是 O(log n),远优于链表的 O(n)。
-
当桶中的元素个数因为删除而低于另一个阈值(
UNTREEIFY_THRESHOLD = 6)时,红黑树会再转换回链表,以节省内存。
这一改进极大地提升了HashMap在面对极端哈希冲突时的性能。
6.4 Map接口的功能增强
JDK 8还为Map接口添加了许多实用的默认方法,让操作映射变得更简单:
-
getOrDefault(Object key, V defaultValue):获取键对应的值,如果键不存在,返回一个默认值。 -
putIfAbsent(K key, V value):只有键不存在或值为null时,才放入新值。 -
computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction):如果键不存在,则使用提供的函数计算一个新值并放入Map中。这对于实现缓存非常有用。 -
forEach(BiConsumer<? super K, ? super V> action):简化了遍历Map键值对的方式。
第七章:便利性增强与性能优化(JDK 9 - 20)
7.1 JDK 9:不可变集合的便利工厂方法(2017年)
在JDK 9之前,创建一个包含几个元素的不可变集合(List、Set、Map)代码相对冗长。JDK 9引入了新的静态工厂方法of(),极大地简化了这一过程。
java
// 之前的方式 (JDK 8)
List<String> list = Collections.unmodifiableList(Arrays.asList("a", "b", "c"));
Set<String> set = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("x", "y")));
// JDK 9 的方式
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("x", "y");
Map<String, Integer> map = Map.of("key1", 1, "key2", 2);
// 或对于超过10个条目,使用 Map.ofEntries()
这些方法返回的是真正不可变的、高度优化的集合,简洁而强大。
7.2 JDK 10(2018年)
List.copyOf()、Set.copyOf()、Map.copyOf():从现有集合创建不可变副本的静态工厂方法。
7.3 JDK 11(2018年)
作为LTS版本,主要进行性能优化和微调。String类新增的lines()、repeat()等方法可以与集合框架结合使用,但集合框架本身的API变化不大。
7.4 JDK 16(2021年)
Stream.toList():可以直接将Stream收集为不可变List,无需通过collect(Collectors.toList())。
java
List<String> result = stream.map(String::toUpperCase).toList();
7.5 JDK 17(2021年):LTS性能优化
JDK 17作为另一个LTS版本,进一步优化了集合框架的性能:
-
并发访问性能提升 :优化
HashMap的线程安全处理。 -
内存分配优化:减少了对象创建时的内存碎片,提高了GC效率。
-
java.util.concurrent包优化:进一步提升了高并发环境下的吞吐量。
第八章:有序集合的统一------JDK 21(2023年)
2023年9月发布的JDK 21,为集合框架带来了两项重要改进。
8.1 虚拟线程适配
-
在高并发场景下,集合类与虚拟线程结合,实现了更高效的资源利用。
-
ConcurrentHashMap等并发集合与虚拟线程协同工作,性能进一步提升。
8.2 SequencedCollection系列接口的引入(JEP 431)
JDK 21引入了**SequencedCollection、SequencedSet和SequencedMap** 接口。这些新接口旨在为那些有明确相遇顺序的集合(如List、Deque、LinkedHashSet、LinkedHashMap、SortedSet、SortedMap)提供一套统一的、更强大的操作API。
核心思想:为"有序"的集合提供获取、添加和删除两端元素的统一方法。
改造前的痛点:
| 集合类型 | 获取第一个元素 | 获取最后一个元素 |
|---|---|---|
List |
list.get(0) |
list.get(list.size() - 1) |
Deque |
deque.getFirst() |
deque.getLast() |
SortedSet |
sortedSet.first() |
sortedSet.last() |
LinkedHashSet |
linkedHashSet.iterator().next() |
缺失(需遍历全部) |
新增接口:
SequencedCollection(继承自Collection,由List和Deque实现)
java
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed(); // 返回逆序视图
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
SequencedSet(继承自Set和SequencedCollection)
java
interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
SequencedSet<E> reversed(); // 协变覆盖
}
SequencedMap(继承自Map)
java
interface SequencedMap<K,V> extends Map<K,V> {
SequencedMap<K,V> reversed(); // 逆序视图
SequencedSet<K> sequencedKeySet();
SequencedCollection<V> sequencedValues();
SequencedSet<Entry<K,V>> sequencedEntrySet();
Entry<K,V> firstEntry();
Entry<K,V> lastEntry();
Entry<K,V> pollFirstEntry();
Entry<K,V> pollLastEntry();
V putFirst(K k, V v);
V putLast(K k, V v);
}
这一改进解决了长期以来的问题:现在可以统一地获取有序集合的首尾元素,并且可以方便地进行逆序遍历和逆序流处理。
java
// 现在,LinkedHashSet也可以轻松获取最后一个元素
LinkedHashSet<String> set = new LinkedHashSet<>();
set.add("a"); set.add("b"); set.add("c");
String last = set.getLast(); // "c"
// 逆序流处理
set.reversed().stream().forEach(System.out::println);
第九章:集合框架完整体系与核心知识点
9.1 完整继承体系(截至JDK 21)
Collection接口体系(完整版):
text
Collection (接口)
├── List (接口)
│ ├── ArrayList (类)
│ ├── LinkedList (类) —— 同时实现了 Deque
│ ├── Vector (类) —— 遗留,线程安全
│ │ └── Stack (类) —— 遗留
│ ├── CopyOnWriteArrayList (类) —— 并发
│ └── AbstractList (抽象类)
│ └── AbstractSequentialList (抽象类)
├── Set (接口)
│ ├── HashSet (类)
│ │ └── LinkedHashSet (类)
│ ├── SortedSet (接口)
│ │ └── NavigableSet (接口) —— JDK 6
│ │ └── TreeSet (类)
│ ├── EnumSet (抽象类) —— JDK 5
│ │ ├── RegularEnumSet (类) —— 包私有
│ │ └── JumboEnumSet (类) —— 包私有
│ ├── CopyOnWriteArraySet (类) —— 并发
│ ├── ConcurrentSkipListSet (类) —— 并发
│ └── AbstractSet (抽象类)
├── Queue (接口) —— JDK 5
│ ├── LinkedList (类)
│ ├── PriorityQueue (类)
│ ├── Deque (接口) —— JDK 6
│ │ ├── LinkedList (类)
│ │ ├── ArrayDeque (类)
│ │ ├── ConcurrentLinkedDeque (类) —— 并发
│ │ └── LinkedBlockingDeque (类) —— 并发
│ ├── BlockingQueue (接口) —— 并发
│ │ ├── ArrayBlockingQueue (类)
│ │ ├── LinkedBlockingQueue (类)
│ │ ├── PriorityBlockingQueue (类)
│ │ ├── SynchronousQueue (类)
│ │ └── DelayQueue (类)
│ └── TransferQueue (接口) —— JDK 7
│ └── LinkedTransferQueue (类)
└── SequencedCollection (接口) —— JDK 21
├── List (接口) —— 已实现
├── Deque (接口) —— 已实现
└── SequencedSet (接口) —— JDK 21
├── NavigableSet (接口)
└── LinkedHashSet (类) —— 新增实现
Map接口体系(完整版):
text
Map (接口)
├── HashMap (类)
│ └── LinkedHashMap (类)
├── Hashtable (类) —— 遗留,线程安全
│ └── Properties (类)
├── SortedMap (接口)
│ └── NavigableMap (接口) —— JDK 6
│ └── TreeMap (类)
├── EnumMap (类) —— JDK 5
├── WeakHashMap (类) —— JDK 1.4
├── IdentityHashMap (类) —— JDK 1.4
├── ConcurrentMap (接口) —— 并发
│ ├── ConcurrentHashMap (类)
│ └── ConcurrentNavigableMap (接口)
│ └── ConcurrentSkipListMap (类)
├── AbstractMap (抽象类)
└── SequencedMap (接口) —— JDK 21
├── LinkedHashMap (类) —— 新增实现
├── NavigableMap (接口)
└── ConcurrentNavigableMap (接口)
9.2 工具类详解
java.util.Collections工具类:
-
排序算法 :
sort()、reverse()、shuffle() -
查找算法 :
binarySearch() -
集合操作 :
max()、min()、frequency()、disjoint() -
包装器 :
synchronizedXXX()(同步包装)、unmodifiableXXX()(不可修改视图)、checkedXXX()(类型安全视图)
java.util.Arrays工具类:
-
数组操作 :
sort()、parallelSort()(JDK 8)、binarySearch() -
填充与复制 :
fill()、copyOf()、copyOfRange() -
比较与转换 :
equals()、deepEquals()、asList() -
哈希码 :
hashCode()、deepHashCode() -
字符串表示 :
toString()、deepToString()
9.3 迭代器演变
| 版本 | 迭代器类型 | 特性 |
|---|---|---|
| JDK 1.0 | Enumeration |
只有hasMoreElements()和nextElement(),不支持移除 |
| JDK 1.2 | Iterator |
增加remove()方法,支持遍历时移除元素 |
| JDK 1.2 | ListIterator |
支持双向遍历,提供previous()、add()、set()等方法 |
| JDK 8 | Spliterator |
支持并行遍历,用于Stream API |
| JDK 21 | SequencedCollection |
通过reversed()获得逆序迭代器 |
9.4 各实现类扩容机制
| 实现类 | 默认初始容量 | 扩容机制 | 线程安全 |
|---|---|---|---|
ArrayList |
0(懒加载,首次添加时变为10) | 1.5倍 | 否 |
Vector |
10 | 2倍(可指定增量) | 是 |
HashMap |
16 | 2倍 | 否 |
Hashtable |
11 | 2倍+1 | 是 |
ConcurrentHashMap |
16 | 2倍 | 是(高效并发) |
LinkedHashMap |
16 | 2倍 | 否 |
第十章:设计哲学与争议(官方FAQ解读)
10.1 核心设计原则
-
面向接口编程 :所有的核心数据结构都由接口(
List、Set、Map)定义,实现类只提供具体的实现细节。这使得代码具有极高的灵活性和可扩展性。 -
提供通用实现:为每个核心接口提供了多种高性能的、开箱即用的通用实现。
-
算法与数据结构分离 :所有的通用算法都被封装在
Collections工具类中,可以应用于任何实现了相应接口的集合。 -
持续进化:从泛型到Stream API,再到并发集合和顺序操作接口,集合框架始终在吸收新的语言特性和编程范式。
10.2 最受争议的设计决策
问题 :为什么不直接在核心接口中支持不变性,从而取消可选操作(以及UnsupportedOperationException)?
官方设计FAQ的回答 :
这是整个API中最具争议的设计决定。静态(编译时)类型检查是非常可取的,但支持不变性会导致接口层次结构的爆炸性增长,而且并不能完全消除运行时异常。
Doug Lea(早期尝试过反映可变性区别的集合包)根据用户经验,不再认为这是可行的方法。用他的话来说:"尽管我很痛苦,但在Java中,强静态类型不适用于集合接口。"
如果要在类型层次结构中反映可修改性,需要新增ModifiableCollection、ModifiableSet、ModifiableList、ModifiableMap等接口。还要考虑数组这类"固定大小"的List,需要新增VariableSizeList。还要考虑日志这类"只追加"的序列。还要考虑真正不可变的集合(与不可修改不同)。最终会导致20多个接口和5种迭代器。
二十五年后的反思 :
《Java泛型与集合(第二版)》指出:即使是适度添加的功能(如不可变集合),也会因为需要实现集合API的突变方法而受到束缚------仅仅抛出UnsupportedOperationException是一种"可怕的黑客行为"。这反映了集合框架二十多年前的高度命令式设计与当前趋向于更函数式编程风格的趋势之间的基本不兼容性。
10.3 其他设计问答
-
Q: 为什么
Map不继承Collection?A: 映射(mappings)不是集合(collections),集合也不是映射。如果
Map是Collection,元素是什么?合理的答案只有"键值对",但这提供了非常有限且不实用的Map抽象。 -
Q: 为什么
Iterator不继承Enumeration?A:
Enumeration的方法名太长,且不够理想。既然我们正在添加方法并创建全新的框架,如果不抓住机会改进命名,那就太愚蠢了。 -
Q: 为什么不用Bean命名风格保持一致性?
A: 集合API会被非常频繁地使用,通常一行代码会多次调用方法。因此方法名应该简短。例如,
iterator()、hasNext()、next()这些短名称使得循环可以写在一行内。 -
Q: 为什么
Collection不继承Cloneable和Serializable?A: 许多集合实现会有
clone()方法,但要求所有集合都支持克隆是错误的。例如,克隆一个由TB级SQL数据库支持的集合意味着什么?应该让客户端决定创建什么类型的集合,然后使用addAll()方法复制元素。
第十一章:集合框架演进全景图
11.1 发展时间线
| 年份 | JDK版本 | 核心演进 | 关键特性与新增类 |
|---|---|---|---|
| 1995 | - | 社区探索 | Doug Lea发布Collections包 |
| 1996 | - | 社区探索 | ObjectSpace发布JGL |
| 1996-1997 | 1.0 / 1.1 | 混沌期 | Vector、Hashtable、Enumeration、Stack、BitSet |
| 1998 | 1.2 | 集合框架诞生 | Collection、List、Set、Map、ArrayList、HashSet、HashMap、TreeSet、TreeMap、Collections、Arrays |
| 2000 | 1.3 | 巩固 | TreeSet/TreeMap稳定,Iterator普及,Collections工具类完善 |
| 2002 | 1.4 | 扩充 | LinkedHashSet、LinkedHashMap、IdentityHashMap、WeakHashMap |
| 2004 | 5 | 泛型革命 + 并发集合 | 泛型、EnumSet、java.util.concurrent包:ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue系列、Queue接口、PriorityQueue |
| 2006 | 6 | 导航接口 | NavigableSet、NavigableMap、Deque、ArrayDeque |
| 2011 | 7 | 传输队列 | TransferQueue、LinkedTransferQueue |
| 2014 | 8 | 函数式演进 + 性能优化 | Lambda、Stream API、默认方法、Map增强、HashMap红黑树化、Spliterator |
| 2017 | 9 | 便利性 | 不可变集合的List.of()、Set.of()、Map.of()工厂方法 |
| 2018 | 10 | 不可变副本 | List.copyOf()、Set.copyOf()、Map.copyOf() |
| 2021 | 16 | Stream增强 | Stream.toList() |
| 2021 | 17 | LTS性能优化 | 并发性能优化、内存分配优化 |
| 2023 | 21 | 有序集合统一 | 虚拟线程适配、SequencedCollection、SequencedSet、SequencedMap接口 |
11.2 各版本新增特性速查
| 版本 | 新增特性 |
|---|---|
| JDK 1.2 | 集合框架诞生,核心接口与实现 |
| JDK 1.4 | LinkedHashSet、LinkedHashMap、IdentityHashMap、WeakHashMap |
| JDK 5 | 泛型、EnumSet、并发集合、Queue、PriorityQueue |
| JDK 6 | NavigableSet、NavigableMap、Deque、ArrayDeque |
| JDK 7 | TransferQueue、LinkedTransferQueue |
| JDK 8 | Lambda、Stream、默认方法、Map增强、HashMap红黑树化、Spliterator |
| JDK 9 | List.of()、Set.of()、Map.of() |
| JDK 10 | List.copyOf()、Set.copyOf()、Map.copyOf() |
| JDK 16 | Stream.toList() |
| JDK 21 | SequencedCollection、SequencedSet、SequencedMap |
11.3 第三方生态发展
-
Jakarta Commons Collections (2001):提供
Bag、LRUMap、FastHashMap等特殊实现 -
JGL 4.0(2002):与Collections Framework集成,提供50多种算法
-
Google Guava (2007-今):提供
ImmutableList、Multiset、BiMap、Table等丰富集合工具
第十二章:面试题库与实战场景
5道难度递增的基础面试题
第1题:Java集合框架是什么?它是在哪个JDK版本中引入的?(难度⭐)
参考答案 :
Java集合框架是一套用于存储和操作对象组的统一架构,它提供了接口(如List、Set、Map)、通用实现(如ArrayList、HashSet、HashMap)以及操作算法(Collections类)。
它是在JDK 1.2 中正式引入的,用以替代之前零散的Vector、Hashtable等特设类。这套框架主要由Joshua Bloch设计和开发。
第2题:Collection和Collections有什么区别?(难度⭐⭐)
参考答案:
-
Collection:是Java集合框架的根接口之一(与Map接口并列)。它定义了集合(如List和Set)最基本的行为,如添加、删除元素、获取大小等。 -
Collections:是一个工具类 ,位于java.util包中。它完全由static方法组成,用于对集合进行操作或返回包装后的集合。例如,Collections.sort()用于排序,Collections.unmodifiableList()用于返回一个不可修改的列表视图。
第3题:ArrayList和LinkedList的区别是什么?分别在什么场景下使用?(难度⭐⭐⭐)
参考答案 :
ArrayList和LinkedList都是List接口的实现,但内部结构和性能差异巨大。
| 维度 | ArrayList |
LinkedList |
|---|---|---|
| 底层数据结构 | 动态数组 (Object[]) | 双向链表 (Node) |
| 随机访问 (get/set) | 极快,时间复杂度 O(1) | 较慢,需要遍历链表,时间复杂度 O(n) |
| 插入/删除 | 在末尾很快(均摊 O(1));在中间或开头较慢,需要移动元素 O(n) | 在两端很快,但在中间较慢,需要先找到位置 O(n),但修改指针本身是 O(1) |
| 内存占用 | 相对更紧凑(只存储元素) | 更大(每个节点额外存储前后指针) |
使用场景:
-
ArrayList:适用于需要频繁随机访问 元素,以及主要在列表末尾添加或删除元素的场景。 -
LinkedList:适用于需要频繁在列表头部或中间插入/删除元素,并且主要以顺序方式(迭代)访问元素的场景。也可以作为队列或双端队列使用。
第4题:HashMap在JDK 8中做了哪些重要改进以提升性能?(难度⭐⭐⭐⭐)
参考答案 :
HashMap在JDK 8中经历了一次重要的性能优化,主要是为了应对哈希碰撞攻击。
在JDK 7及之前,HashMap的底层结构是数组+链表。当大量键的哈希值相同时,它们会被放入同一个桶中,形成一个长链表,查找性能会退化到 O(n)。
JDK 8的改进是引入了**"链表转红黑树"**的机制:
-
当一个桶中的元素个数超过阈值(默认为
TREEIFY_THRESHOLD = 8)且数组长度大于64时,HashMap会将这个桶的存储结构从链表转换为红黑树。 -
红黑树的查找、插入和删除性能是 O(log n),远优于链表的 O(n)。
-
当桶中的元素个数因为删除而低于另一个阈值(
UNTREEIFY_THRESHOLD = 6)时,红黑树会再转换回链表,以节省内存。
这一改进极大地提升了HashMap在面对极端哈希冲突时的性能。
第5题:什么是fail-fast迭代器?什么是fail-safe迭代器?(难度⭐⭐⭐⭐)
参考答案:
-
fail-fast(快速失败) :是java.util包下集合(如ArrayList、HashMap)的默认迭代行为。当通过迭代器遍历一个集合时,如果在遍历过程中集合的结构被直接修改(例如通过集合对象的add()或remove()方法),迭代器会立即抛出ConcurrentModificationException,从而"快速失败",避免后续出现不确定的行为。这是通过维护一个modCount修改计数器实现的。 -
fail-safe(安全失败) :是java.util.concurrent包下并发集合(如ConcurrentHashMap、CopyOnWriteArrayList)的迭代行为。这类迭代器不会直接抛出ConcurrentModificationException。它们通过在迭代开始时基于当前集合的一个快照 进行遍历,或者在遍历过程中对底层的修改是无锁的、非阻塞的。因此,即使迭代器创建后原集合被修改,也不会影响当前正在进行的遍历。但这种方式的代价是不保证能读到迭代器创建后的最新修改,并且会有额外的内存开销。
3道实战场景题
场景1:数据去重与排序
问题 :你有一个包含大量订单对象的列表List<Order>,每个订单都有orderId(唯一)、createTime和amount。现在需要去除列表中重复的订单(基于orderId),并按createTime倒序排序,最后返回处理后的列表。你会如何用Java集合实现?
考察点 :Set去重、Comparator排序、Stream API
参考思路:
java
// 1. 使用 Set 去重 (基于 orderId)
// 假设 Order 类已经定义了 equals() 和 hashCode() 基于 orderId
Set<Order> uniqueOrders = new LinkedHashSet<>(orders); // 使用 LinkedHashSet 保留原始顺序
// 2. 转换为 List 并进行排序
List<Order> sortedOrders = new ArrayList<>(uniqueOrders);
sortedOrders.sort(Comparator.comparing(Order::getCreateTime).reversed());
// 或者,使用 Stream API 更简洁地一步完成
List<Order> result = orders.stream()
.collect(Collectors.collectingAndThen(
Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(Order::getOrderId))),
ArrayList::new))
.stream()
.sorted(Comparator.comparing(Order::getCreateTime).reversed())
.collect(Collectors.toList());
场景2:统计词频
问题 :编写一个方法,接收一段英文文本(String),返回其中每个单词出现的次数(不区分大小写),并且只输出出现次数超过3次的单词,按次数降序排列。
考察点 :Map计数、HashMap、TreeMap或Stream分组、排序
参考思路:
java
public Map<String, Long> countFrequentWords(String text) {
// 1. 分割文本为单词数组
String[] words = text.toLowerCase().split("\\W+"); // 正则分割非单词字符
// 2. 使用 Stream 进行分组计数
Map<String, Long> wordCount = Arrays.stream(words)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
// 3. 筛选出现次数 > 3 的单词,并按次数降序排序
// LinkedHashMap 可以保持排序后的顺序
return wordCount.entrySet().stream()
.filter(entry -> entry.getValue() > 3)
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(e1, e2) -> e1,
LinkedHashMap::new
));
}
场景3:选择合适的并发集合
问题 :你需要在一个高并发的Web服务器中缓存用户的会话信息。每个用户会话是一个UserSession对象,需要根据sessionId(String类型)快速存取。该缓存会被多个线程并发读写,要求性能尽可能高。你会选择哪个Map实现?为什么?如果还需要定期移除不活跃的会话,又该如何设计?
考察点 :ConcurrentHashMap、缓存策略、ScheduledExecutorService
参考思路:
-
高性能并发读写 :首选**
ConcurrentHashMap<String, UserSession>**。-
为什么不是
Hashtable:Hashtable的所有方法都是同步的,在高并发下会导致严重的锁竞争,性能极差。 -
为什么不是
Collections.synchronizedMap(new HashMap<>()):它返回的同步包装器也相当于一个全局锁,性能同样不佳。 -
ConcurrentHashMap的优势:它采用了细粒度的锁机制(JDK 7采用锁分段,JDK 8采用CAS+synchronized),允许多个线程并发地读,以及有限的并发写,极大地提升了吞吐量。
-
-
移除不活跃会话:需要一种方式来标记和清理过期会话。
-
方案一 :在
UserSession对象中加入最后访问时间戳lastAccessTime。然后使用一个**ScheduledExecutorService** 定期执行任务,遍历ConcurrentHashMap并移除那些长时间未访问的会话。但遍历一个大Map可能会影响性能。 -
方案二 :使用JDK内置的**
LinkedHashMap** 来构建一个LRU(Least Recently Used)缓存 。通过重写removeEldestEntry方法,可以设定当Map大小超过阈值时,自动移除最久未访问的条目。 -
方案三 :使用专门的缓存库,如Google Guava的CacheBuilder ,它提供了强大的、可配置的、基于LRU或时间过期的缓存功能,且内部实现高度优化,能很好地与
ConcurrentHashMap协同工作。
-
结语
从JDK 1.2的初生牛犊,到如今JDK 21的成熟稳健,Java集合框架已经走过了近三十年的辉煌历程。它始于对混乱的"特设类"的统一,在社区探索中汲取养分,在泛型中涅槃重生,在并发浪潮中开疆拓土,在函数式编程中焕发新颜,在JDK 21中实现了有序集合的统一。它不仅是Java标准库中使用频率最高的API,更是一本生动的教科书,向我们展示了何为优秀的API设计:清晰、一致、可扩展、高性能。
了解这段历史,掌握其演进脉络,不仅能让你在面试中游刃有余,更能让你在面对复杂的数据处理需求时,做出最恰当的技术选择,写出更优雅、更健壮的代码。
参考资料:
-
Wikipedia:Java集合框架
-
腾讯云开发者社区:Java集合框架知识整理
-
O'Reilly:《Java泛型与集合(第二版)》设计回顾章节
-
亿速云:Java集合框架发展历史
-
华为云社区:Java Review (二十三、集合-----概述)
-
Oracle官方文档:Java Collections API Design FAQ
-
百度百科:JAVA集合框架
-
百度百科:Java集合类
-
Blogjava:介绍三个集合容器库