抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Java的泛型(Generics)和C++的模板(Templates)都是为了实现参数化类型,让代码更加通用。但它们的实现机制完全不同,导致了很多行为上的差异。本文深入对比两者的异同。

基本语法对比

泛型类/模板类

1
2
3
4
5
6
7
8
9
10
11
// Java泛型类
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
// C++模板类
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
// Java泛型方法
public <T> T getFirst(List<T> list) {
return list.get(0);
}
1
2
3
4
5
// C++模板函数
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; // T变成Object
public Object get() { return value; }
}

Box stringBox = new Box(); // 只有一个Box类
Box intBox = new Box(); // 同一个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
// Java泛型只能用引用类型
List<Integer> list = new ArrayList<>(); // OK
List<int> list = new ArrayList<>(); // 编译错误!

// 基本类型必须用包装类
Box<int> box; // 错误
Box<Integer> box; // OK,自动装箱拆箱

C++:可以是任何类型

1
2
3
4
5
6
7
// C++模板可以用任何类型
std::vector<int> vec; // 基本类型OK
std::vector<std::string> vec2; // 类类型OK

Box<int> box; // OK
Box<double> box2; // OK
Box<MyClass> box3; // OK

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; // 10个int的数组
Array<int, 20> arr2; // 20个int的数组
// arr1和arr2是不同的类型!

Java泛型做不到这一点:

1
2
3
// Java无法做到
// 不能把数值作为泛型参数
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()); // true

// 无法在运行时获取泛型类型
if (strings instanceof List<String>) { } // 编译错误!

// 只能这样判断
if (strings instanceof List<?>) { } // OK,但没啥用
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)); // false

// 可以区分类型
if (typeid(container) == typeid(std::vector<int>)) {
// 确实是vector<int>
}
1
2
3
4
5
// 可以直接创建数组
template<typename T>
class Container {
T* data = new T[10]; // OK
};

代码膨胀问题

Java:无代码膨胀

由于类型擦除,无论用多少种类型参数,只有一份字节码:

1
2
3
4
5
List<String> a;
List<Integer> b;
List<Double> c;
List<MyClass> d;
// 全部使用同一个ArrayList类的字节码

C++:代码膨胀

每种类型实例化都生成新代码:

1
2
3
4
5
std::vector<int> a;
std::vector<double> b;
std::vector<std::string> c;
std::vector<MyClass> d;
// 生成4份不同的代码!

这可能导致:

  • 编译时间长
  • 二进制文件体积大
  • 但运行时性能更好(无类型转换开销)

模板特化 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); // 默认实现
}
};

// 针对string的特化
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);          // "42"
Serializer<std::string>::serialize("hi"); // "\"hi\""
Serializer<int*>::serialize(nullptr); // "null"

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
// 上界约束:T必须是Number或其子类
public <T extends Number> double sum(List<T> list) {
double sum = 0;
for (T num : list) {
sum += num.doubleValue(); // 可以调用Number的方法
}
return sum;
}

// 多重约束
public <T extends Comparable<T> & Serializable> void process(T item) {
// T必须同时实现Comparable和Serializable
}

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>

// 定义concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

// 使用concept约束
template<Numeric T>
T sum(std::vector<T>& list) {
T total = 0;
for (T num : list) {
total += num;
}
return total;
}

// 或者用requires子句
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
// 晦涩的SFINAE方式
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); // 编译错误:Object不满足Comparable<Object>约束

C++:错误信息可能很长

模板错误发生在实例化时,错误信息可能非常长:

1
2
3
4
5
6
7
8
template<typename T>
void sort(std::vector<T>& vec) {
std::sort(vec.begin(), vec.end()); // 要求T可比较
}

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); // 3(共享同一个静态变量)

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; // 1
std::cout << Counter<int>::count; // 1
std::cout << Counter<double>::count; // 1
// 每种类型有自己的静态变量!

继承行为

Java:泛型不协变

1
2
3
4
5
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // 编译错误!

// 虽然String是Object的子类
// 但List<String>不是List<Object>的子类型

需要使用通配符:

1
List<? extends Object> objects = strings;  // OK

C++:模板实例化是独立类型

1
2
3
4
5
std::vector<std::string> strings;
std::vector<void*> objects = strings; // 编译错误

// vector<string>和vector<void*>是完全不同的类型
// 没有任何继承关系

总结对比表

特性 Java泛型 C++模板
实现机制 类型擦除 模板实例化
类型参数 仅引用类型 任何类型
非类型参数 不支持 支持
运行时类型 丢失 保留
代码膨胀
模板特化 不支持 支持
静态成员 共享 每实例独立
类型约束 extends/super Concepts/SFINAE
编译错误 清晰 可能很长
创建泛型数组 不可直接创建 可以

各自的优缺点

Java泛型

优点:

  • 无代码膨胀,字节码体积小
  • 与旧代码兼容(可以把List<String>传给List
  • 编译错误信息清晰

缺点:

  • 运行时类型信息丢失
  • 不能用基本类型(需要装箱)
  • 不支持特化
  • 某些操作受限(如创建泛型数组)

C++模板

优点:

  • 运行时类型信息完整
  • 支持任何类型(包括基本类型)
  • 支持模板特化,更灵活
  • 编译时计算,运行时零开销

缺点:

  • 代码膨胀
  • 编译时间长
  • 错误信息难以理解(C++20前)
  • 模板代码通常要放在头文件中

设计哲学的差异

Java泛型的设计目标是向后兼容

  • JDK 5引入泛型时,要保证旧的非泛型代码能继续工作
  • 类型擦除使得List<String>List在运行时兼容

C++模板的设计目标是零开销抽象

  • 泛型代码应该和手写的特定类型代码一样高效
  • 编译时生成特化代码,运行时无额外开销

理解这些差异,有助于在使用时做出正确的设计决策。