前言
严格来说,Java 没有 "STL"。STL(Standard Template Library)最初指 SGI 那套以容器、迭代器、算法、函数对象为核心的 C++ 模板库,后来被吸纳进 C++ 标准库;Java 中与之对应的东西叫集合框架(Collections Framework),位于java.util包。把两者放在一起类比是学习的好方法,但必须清楚它们建立在两套完全不同的语言机制上:Java 的泛型在编译后擦除,元素只能是引用类型(基本类型要装箱);C++ 的模板按类型实例化,容器直接按值存储对象,配合 RAII 管理生命周期。
这些差异会直接改变代码写法。Java 的ArrayList<Integer>里装的是Integer对象的引用,get返回引用;C++ 的std::vector<int>里是连续的int,operator[]返回引用,拷贝容器就是深拷贝。Java 的HashMap依赖equals与hashCode;C++ 的std::unordered_map依赖operator==与std::hash特化。Java 有垃圾回收,C++ 没有,容器里放指针时所有权要自己设计。
本文先讲两套体系的根本差异,再逐个容器对照,然后比较迭代器、算法与比较器,最后给出语言机制层面的注意事项和一段并列示例。类比是为了迁移已有知识,不是把 Java 的习惯生搬硬套到 C++。
一、两套体系的根本差异
泛型的实现方式不同。Java 泛型采用类型擦除(type erasure):ArrayList<String>和ArrayList<Integer>在运行时是同一个类,类型参数只存在于编译期,因此不能new T[],也不能对基本类型做特化。C++ 模板在编译期为每组实参生成独立代码,std::vector<int>与std::vector<double>是不同类型,各自有完整的实现,代价是代码体积可能膨胀。
元素的存储方式不同。Java 集合只能存引用,int放进ArrayList<Integer>会自动装箱成Integer对象,取出时再拆箱;大量小整数会带来额外的对象开销。C++ 容器按值存储,std::vector<int>的元素就是int本身,内存连续,没有装箱。
生命周期管理不同。Java 依赖垃圾回收,集合持有引用即可,不用关心何时释放。C++ 容器析构时销毁其中的元素;如果元素是裸指针,容器不会delete它们,必须用std::unique_ptr等 RAII 类型,或手动管理。
相等与哈希的约定不同。Java 要求重写equals时同时重写hashCode,否则哈希容器行为错误。C++ 没有统一的"对象基类",需要为自定义类型提供operator==,并为std::hash提供特化(或传入自定义哈希函数对象),两者必须保持一致。
| 哈希表 | HashMap | std::unordered_map |
| 哈希集合 | HashSet | std::unordered_set |
| 优先队列 | PriorityQueue | std::priority_queue |
| 比较 | Comparator/Comparable | 比较器函数对象 /operator< |
二、容器逐一对照
ArrayList与std::vector。两者都是动态数组,支持 O(1) 随机访问,尾部追加摊还 O(1),中间插入删除 O(n)。差异在于扩容策略:Java 的实现通常按一定比例增长,C++ 标准则完全不规定扩容因子,只要求摊还常数。依赖"扩容后容量是多少"的代码在两边都不可移植。C++ 的vector还提供reserve和capacity,Java 的ArrayList有ensureCapacity,语义相近。
LinkedList与std::list。都是双向链表,已知位置的插入删除是 O(1),按下标访问是 O(n)。Java 的LinkedList还实现了Deque接口,可以当双端队列用;C++ 的std::list没有随机访问迭代器,因此不能用std::sort,要用成员函数sort。
ArrayDeque与std::deque。都是双端队列,两端插入删除 O(1),支持随机访问。Java 官方推荐用ArrayDeque代替老旧的Stack类;C++ 的std::stack是一个适配器,默认基于std::deque实现,只暴露栈顶操作。
HashMap与std::unordered_map。都是哈希表,平均查找 O(1),遍历顺序都不保证。Java 8 之后的HashMap在桶内元素较多时会转成红黑树以限制最坏情况;C++ 标准只要求平均复杂度,具体实现(链地址法或开放寻址)由库决定。C++ 的unordered_map需要为键类型提供std::hash与operator==。
TreeMap与std::map。都是按键有序的映射,查找、插入、删除 O(log n)。Java 的TreeMap基于红黑树;C++ 的std::map标准只要求平衡二叉搜索树的复杂度,常见实现也是红黑树,但不应依赖具体结构。C++ 的map用std::less<Key>比较键,可以传入自定义比较器。
HashSet/TreeSet与unordered_set/set。关系与映射完全平行:哈希版无序、平均 O(1),有序版按比较器排序、O(log n)。
PriorityQueue与std::priority_queue。都是二叉堆,取堆顶 O(1),插入删除 O(log n)。有一个必须记住的差异:Java 的PriorityQueue默认是最小堆,C++ 的priority_queue默认是最大堆。C++ 要得到最小堆得写std::priority_queue<int, std::vector<int>, std::greater<int>>。
没有直接对应的情况。Java 的LinkedHashMap保持插入顺序(或访问顺序),C++ 标准库没有等价容器;需要时得自己组合list与unordered_map。Java 的ConcurrentHashMap提供细粒度并发,C++ 标准库没有并发容器,只能用互斥量包装普通容器,或引入第三方库。Java 的Vector和Hashtable是早期同步容器,现已不推荐,C++ 没有对应物。
三、迭代器、算法与比较器
Java 的Iterator是一个对象,接口是hasNext()、next()、remove();C++ 的迭代器模仿指针,用*it取值、++it前进、it != end判断结束。C++ 的算法接收半开区间[first, last),Java 的集合操作多以成员方法或Collections静态方法提供。
// 适用于 Java 17
import java.util.*;
record Employee(String name, int age) {}
public class Demo {
public static void main(String[] args) {
List<Employee> staff = new ArrayList<>(List.of(
new Employee("Alice", 30),
new Employee("Bob", 24),
new Employee("Carol", 35)
));
staff.sort(Comparator.comparingInt(Employee::age));
Map<String, Integer> byName = new TreeMap<>();
for (Employee e : staff) {
byName.put(e.name(), e.age());
}
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.add(3);
minHeap.add(1);
System.out.println(minHeap.peek()); // 1
}
}
对应的 C++ 写法:
// 适用于 C++17
#include <algorithm>
#include <functional>
#include <iostream>
#include <map>
#include <queue>
#include <string>
#include <unordered_map>
#include <vector>
struct Employee {
std::string name;
int age;
};
int main() {
std::vector<Employee> staff{
{"Alice", 30}, {"Bob", 24}, {"Carol", 35}
};
std::sort(staff.begin(), staff.end(),
[](const Employee& a, const Employee& b) {
return a.age < b.age;
});
std::map<std::string, int> by_name;
for (const Employee& e : staff) {
by_name[e.name] = e.age;
}
std::unordered_map<std::string, int> fast;
for (const auto& kv : by_name) {
fast.emplace(kv.first, kv.second);
}
// 默认是最大堆;这里显式指定 greater 得到最小堆
std::priority_queue<int, std::vector<int>, std::greater<int>> min_heap;
min_heap.push(3);
min_heap.push(1);
std::cout << min_heap.top() << '\n'; // 1
return 0;
}
比较器的契约两边也有差别。Java 的Comparator要求满足自反性、对称性、传递性,违反时sort可能抛IllegalArgumentException。C++ 的比较器要求严格弱序,违反时行为未定义,程序可能崩溃也可能静默产生错误顺序——C++ 不会替你检查。
另一个关键差异在迭代器失效。Java 的集合迭代器大多是 fail-fast 的:在迭代过程中结构性修改集合,下一次next()通常会抛ConcurrentModificationException(这是尽力而为的检测,不是并发安全保障)。C++ 的迭代器失效没有任何运行时检查,使用失效迭代器是未定义行为。两者都要求"不要在遍历时随意改容器",但 Java 会当场报错,C++ 可能沉默地出错。
四、语言机制差异与迁移建议
相等与哈希。Java 的equals是方法,默认比较引用,通常要重写;hashCode必须与之一致。C++ 的operator==可以是非成员函数,std::hash是模板特化。为自定义类型写哈希容器时,要保证"相等的对象哈希值相同",否则查找会失败。
多态与切片。Java 的List<Shape>存的是引用,放Circle对象天然多态。C++ 的std::vector<Shape>按值存储,放入Circle会发生对象切片,只剩基类部分;要保留多态必须存std::unique_ptr<Shape>或std::shared_ptr<Shape>。
所有权。Java 不需要考虑谁负责释放;C++ 容器析构只销毁元素本身。容器里放裸指针时,容器销毁不会释放所指对象,容易泄漏。默认用值类型,需要多态时用std::unique_ptr,需要共享时用std::shared_ptr并警惕循环引用。
算法风格。Java 8 起有 Stream API,C++20 起有 ranges,两者都支持链式处理,但底层模型不同:Stream 是惰性管道,ranges 是编译期约束的迭代器组合。跨语言迁移时不要把stream().filter().collect()逐字翻译成循环加if,也不必强行模仿,按目标语言的惯用法写即可。
| 存基本类型 | ArrayList<Integer> | std::vector<int>,无装箱 |
| 存多态对象 | List<Shape> | std::vector<std::unique_ptr<Shape>> |
| 自定义键 | 重写equals/hashCode | operator==+std::hash特化 |
| 排序 | Comparator.comparing | lambda 比较器,严格弱序 |
| 遍历时删除 | Iterator.remove() | erase-remove 惯用法 |
| 并发容器 | ConcurrentHashMap | 互斥量包装或第三方库 |
常见坑点
- ❌ 把 Java 的装箱语义带到 C++,以为
std::vector<int>存的是对象指针。✅ C++ 容器按值连续存储,int就是int,拷贝是值拷贝。
- ❌ 在 C++ 里依赖
unordered_map的遍历顺序。✅ 哈希容器无序,需要顺序就用std::map或另建索引。
- ❌ 以为 C++ 的
operator==默认做深比较。✅ 自定义类型要自己定义operator==;容器比较逐元素调用它。
- ❌ 照搬 Java 的
PriorityQueue习惯,以为 C++ 默认是最小堆。✅ C++ 默认最大堆,最小堆要传std::greater<T>。
- ❌ 在 C++ 里遍历容器时删除元素,继续使用旧迭代器。✅ 用 erase-remove,或使用
erase返回的新迭代器。
- ❌ 把 Java 迭代器的 fail-fast 当成 C++ 也有运行时保护。✅ C++ 迭代器失效后使用是未定义行为,没有异常提示。
- ❌ 往
std::vector<Base>里放派生类对象,期待多态。✅ 会发生对象切片;改存std::unique_ptr<Base>。
- ❌ 认为 Java 的泛型擦除和 C++ 模板是一回事,写出
new T[]之类的代码。✅ C++ 模板按类型实例化,但数组大小与类型构造仍需具体类型信息,别混用两套心智模型。
总结
用 Java 的经验理解 C++ 的 STL 是高效的起点,但落点必须回到 C++ 的语义:值语义、手动所有权、严格弱序、未定义行为。把容器名字对应上只是第一步,真正决定代码是否正确的是这四条底层规则。