Java的泛型(Generics)和C++的模板(Templates)都是为了实现参数化类型,让代码更加通用。但它们的实现机制完全不同,导致了很多行为上的差异。本文深入对比两者的异同。
基本语法对比
泛型类/模板类
1 2 3 4 5 6 7 8 9 10 11
| public class Box<T> { private T value;
public void set(T value) { this.value = value; } public T get() { return value; } }
Box<String> box = new Box<>(); box.set("Hello");
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| template<typename T> class Box { private: T value; public: void set(T value) { this->value = value; } T get() { return value; } };
Box<std::string> box; box.set("Hello");
|
泛型方法/模板函数
1 2 3 4
| public <T> T getFirst(List<T> list) { return list.get(0); }
|
1 2 3 4 5
| template<typename T> T getFirst(std::vector<T>& list) { return list[0]; }
|
看起来很相似,但底层实现天差地别。
核心差异:实现机制
Java:类型擦除(Type Erasure)
Java泛型是编译时特性,编译后泛型信息被擦除。
1 2 3 4 5 6 7 8
| public class Box<T> { private T value; public T get() { return value; } }
Box<String> stringBox = new Box<>(); Box<Integer> intBox = new Box<>();
|
1 2 3 4 5 6 7 8
| public class Box { private Object value; public Object get() { return value; } }
Box stringBox = new Box(); Box intBox = new Box();
|
编译器在调用处自动插入强制类型转换:
1 2 3 4 5
| String s = stringBox.get();
String s = (String) stringBox.get();
|
C++:模板实例化(Template Instantiation)
C++模板在编译时为每种类型生成独立的代码。
1 2 3 4 5 6 7 8 9
| template<typename T> class Box { T value; public: T get() { return value; } };
Box<std::string> stringBox; Box<int> intBox;
|
编译后相当于生成了两个完全独立的类:
1 2 3 4 5 6 7 8 9 10 11 12
| class Box_string { std::string value; public: std::string get() { return value; } };
class Box_int { int value; public: int get() { return value; } };
|
对比图示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Java类型擦除: 编译 Box<String> ─────────┐ ├──────▶ Box (单一类,使用Object) Box<Integer> ─────────┘
运行时只有一个Box类,泛型信息丢失
C++模板实例化: 编译 Box<string> ─────────────────▶ Box_string (独立类)
Box<int> ─────────────────▶ Box_int (独立类)
运行时有多个独立的类
|
类型参数的差异
Java:只能是引用类型
1 2 3 4 5 6 7
| List<Integer> list = new ArrayList<>(); List<int> list = new ArrayList<>();
Box<int> box; Box<Integer> box;
|
C++:可以是任何类型
1 2 3 4 5 6 7
| std::vector<int> vec; std::vector<std::string> vec2;
Box<int> box; Box<double> box2; Box<MyClass> box3;
|
C++:支持非类型模板参数
C++模板参数不仅可以是类型,还可以是值:
1 2 3 4 5 6 7 8 9 10 11
| template<typename T, int Size> class Array { T data[Size]; public: int size() { return Size; } };
Array<int, 10> arr1; Array<int, 20> arr2;
|
Java泛型做不到这一点:
1 2 3
|
class Array<T, 10> { }
|
运行时行为差异
Java:运行时类型信息丢失
1 2 3 4 5 6 7 8 9 10 11
| List<String> strings = new ArrayList<>(); List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());
if (strings instanceof List<String>) { }
if (strings instanceof List<?>) { }
|
1 2 3 4 5 6
| T[] array = new T[10];
@SuppressWarnings("unchecked") T[] array = (T[]) new Object[10];
|
C++:运行时类型独立
1 2 3 4 5 6 7 8 9 10
| std::vector<std::string> strings; std::vector<int> integers;
std::cout << (typeid(strings) == typeid(integers));
if (typeid(container) == typeid(std::vector<int>)) { }
|
1 2 3 4 5
| template<typename T> class Container { T* data = new T[10]; };
|
代码膨胀问题
Java:无代码膨胀
由于类型擦除,无论用多少种类型参数,只有一份字节码:
1 2 3 4 5
| List<String> a; List<Integer> b; List<Double> c; List<MyClass> d;
|
C++:代码膨胀
每种类型实例化都生成新代码:
1 2 3 4 5
| std::vector<int> a; std::vector<double> b; std::vector<std::string> c; std::vector<MyClass> d;
|
这可能导致:
- 编译时间长
- 二进制文件体积大
- 但运行时性能更好(无类型转换开销)
模板特化 vs 无特化
C++:支持模板特化
C++可以为特定类型提供特殊实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| template<typename T> class Serializer { public: static std::string serialize(T value) { return std::to_string(value); } };
template<> class Serializer<std::string> { public: static std::string serialize(std::string value) { return "\"" + value + "\""; } };
template<typename T> class Serializer<T*> { public: static std::string serialize(T* value) { return value ? Serializer<T>::serialize(*value) : "null"; } };
|
1 2 3
| Serializer<int>::serialize(42); Serializer<std::string>::serialize("hi"); Serializer<int*>::serialize(nullptr);
|
Java:不支持特化
Java泛型无法为特定类型提供不同实现:
1 2 3 4 5 6 7 8 9
| public class Serializer<T> { public String serialize(T value) { ... } }
public class Serializer<String> { public String serialize(String value) { ... } }
|
只能通过重载或继承来模拟:
1 2 3 4 5 6 7 8 9 10 11
| public class Serializer { public static String serialize(int value) { return String.valueOf(value); }
public static String serialize(String value) { return "\"" + value + "\""; }
}
|
类型约束
Java:extends和super
1 2 3 4 5 6 7 8 9 10 11 12 13
| public <T extends Number> double sum(List<T> list) { double sum = 0; for (T num : list) { sum += num.doubleValue(); } return sum; }
public <T extends Comparable<T> & Serializable> void process(T item) { }
|
C++:Concepts(C++20)
C++20之前主要靠SFINAE(Substitution Failure Is Not An Error),比较晦涩。C++20引入了Concepts:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <concepts>
template<typename T> concept Numeric = std::integral<T> || std::floating_point<T>;
template<Numeric T> T sum(std::vector<T>& list) { T total = 0; for (T num : list) { total += num; } return total; }
template<typename T> requires std::totally_ordered<T> T max(T a, T b) { return a > b ? a : b; }
|
C++20之前:SFINAE
1 2 3 4 5 6
| template<typename T, typename = std::enable_if_t<std::is_arithmetic_v<T>>> T sum(std::vector<T>& list) { }
|
编译错误信息
Java:错误信息清晰
由于泛型约束在编译时检查,错误信息通常比较清晰:
1 2 3 4
| public <T extends Comparable<T>> void sort(List<T> list) { }
List<Object> list = new ArrayList<>(); sort(list);
|
C++:错误信息可能很长
模板错误发生在实例化时,错误信息可能非常长:
1 2 3 4 5 6 7 8
| template<typename T> void sort(std::vector<T>& vec) { std::sort(vec.begin(), vec.end()); }
struct NoCompare { }; std::vector<NoCompare> vec; sort(vec);
|
C++20的Concepts改善了这个问题。
静态成员的差异
Java:泛型类的静态成员被共享
1 2 3 4 5 6 7 8 9 10 11
| public class Counter<T> { public static int count = 0;
public Counter() { count++; } }
new Counter<String>(); new Counter<Integer>(); new Counter<Double>();
System.out.println(Counter.count);
|
C++:每个实例化有独立的静态成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| template<typename T> class Counter { public: static int count; Counter() { count++; } };
template<typename T> int Counter<T>::count = 0;
Counter<std::string> c1; Counter<int> c2; Counter<double> c3;
std::cout << Counter<std::string>::count; std::cout << Counter<int>::count; std::cout << Counter<double>::count;
|
继承行为
Java:泛型不协变
1 2 3 4 5
| List<String> strings = new ArrayList<>(); List<Object> objects = strings;
|
需要使用通配符:
1
| List<? extends Object> objects = strings;
|
C++:模板实例化是独立类型
1 2 3 4 5
| std::vector<std::string> strings; std::vector<void*> objects = strings;
|
总结对比表
| 特性 |
Java泛型 |
C++模板 |
| 实现机制 |
类型擦除 |
模板实例化 |
| 类型参数 |
仅引用类型 |
任何类型 |
| 非类型参数 |
不支持 |
支持 |
| 运行时类型 |
丢失 |
保留 |
| 代码膨胀 |
无 |
有 |
| 模板特化 |
不支持 |
支持 |
| 静态成员 |
共享 |
每实例独立 |
| 类型约束 |
extends/super |
Concepts/SFINAE |
| 编译错误 |
清晰 |
可能很长 |
| 创建泛型数组 |
不可直接创建 |
可以 |
各自的优缺点
Java泛型
优点:
- 无代码膨胀,字节码体积小
- 与旧代码兼容(可以把
List<String>传给List)
- 编译错误信息清晰
缺点:
- 运行时类型信息丢失
- 不能用基本类型(需要装箱)
- 不支持特化
- 某些操作受限(如创建泛型数组)
C++模板
优点:
- 运行时类型信息完整
- 支持任何类型(包括基本类型)
- 支持模板特化,更灵活
- 编译时计算,运行时零开销
缺点:
- 代码膨胀
- 编译时间长
- 错误信息难以理解(C++20前)
- 模板代码通常要放在头文件中
设计哲学的差异
Java泛型的设计目标是向后兼容:
- JDK 5引入泛型时,要保证旧的非泛型代码能继续工作
- 类型擦除使得
List<String>和List在运行时兼容
C++模板的设计目标是零开销抽象:
- 泛型代码应该和手写的特定类型代码一样高效
- 编译时生成特化代码,运行时无额外开销
理解这些差异,有助于在使用时做出正确的设计决策。