在Java中,分为 原始类型(Raw Type) 和 泛型类型(Generic Type) 两种不同的类型,主要区别如下:
-
原始类型:在引入泛型之前,Java集合类(如List、Set、Map等)都是原始类型。它们可以存储任意类型的对象,但是原始类型存储的对象在取出时需要进行强制类型转换,如果转换的目标类型与实际存储的类型不兼容,就会在运行时抛出ClassCastException。这种错误在编译时不会被发现,增加了运行时失败的风险。
-
泛型类型:Java 5 引入了泛型,允许在定义类、接口和方法时使用类型参数。泛型提供了编译时类型安全检查,可以将运行时错误转移到编译时。例如,List
表示这个List只能存储String类型的对象,取出时不需要强制类型转换,并且编译器会确保类型安全。
原始类型是通用的,但是不安全,泛型是有限的通用,相对安全!
在代码审计的时候,对原始类型的使用,也是一个审查项。
两种类型的使用代码示例:
// 原始类型 - 不安全
List rawList = new ArrayList();
rawList.add("hello");
rawList.add(123); // 可以添加不同类型
String str = (String) rawList.get(0); // 需要强制转换
Integer num = (Integer) rawList.get(1); // 运行时可能报错、失败
// 泛型类型 - 安全
List<String> genericList = new ArrayList<>();
genericList.add("hello");
// genericList.add(123); // 编译错误
String str = genericList.get(0); // 自动转换,不需要强制转换
自定义泛型类
// 定义泛型类
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// 使用
Box rawBox = new Box(); // 原始类型
rawBox.set("test"); // 可以设置任何类型
String s = (String) rawBox.get(); // 需要强制转换
Box<String> genericBox = new Box<>(); // 泛型类型
genericBox.set("test");
String s = genericBox.get(); // 自动获取String类型
类型擦除
什么是类型擦除?
Java的泛型是通过类型擦除实现的,这意味着:
- 泛型信息只在编译时存在;
- 运行时所有泛型类型都变为原始类型;
List<String> 和 List<Integer> 在运行时都是 List;
类型擦除的作用:
类型擦除是Java泛型实现的一个折中方案,它使得泛型代码能够与旧版本Java代码兼容(这个是最主要的作用),同时不会带来运行时性能损失。
但是,类型擦除也带来了一些限制,需要在编程时注意。通过一些设计模式(如工厂模式)和反射,可以在一定程度上绕过这些限制。
类型擦除带来的限制:
-
无法使用基本类型作为类型参数:
因为类型擦除后替换为Object,而基本类型不是Object的子类,所以不能使用。
例如,不能创建Box<int>,而必须使用Box<Integer>。 -
无法获取泛型类型的具体类:
由于运行时类型信息被擦除,无法在运行时获取泛型类型的具体类型。例如,不能使用new T(),因为不知道T的具体类型。 -
无法创建泛型数组:
不能直接创建泛型数组,例如new T[10],因为数组在创建时需要知道具体的类型,而泛型类型被擦除后无法确定。 -
方法重载的冲突:
由于类型擦除,两个重载方法可能擦除后变成相同的方法签名,导致编译错误。
绕过类型擦除的限制:
-
使用反射:
通过反射可以在运行时获取泛型类型信息,但需要额外的代码。 -
使用工厂模式:
通过传入类型标签(Class对象)来创建实例。 -
使用泛型数组的变通方法:
使用Array.newInstance(Class, int)来创建泛型数组。
总结
为什么泛型优于原始类型?
| 比较维度 | 原始类型 | 泛型 |
|---|---|---|
| 类型安全 | 无,运行时可能出错 | 编译时检查,更安全 |
| 代码可读性 | 差,需要文档说明类型 | 好,类型信息一目了然 |
| 维护性 | 差,容易引入错误 | 好,重构时编译器帮助检查 |
| 通用性 | 任意类型,使用不好容易出类型错误 | 不支持任意类型,但可通过设计实现安全的通用性 |
| IDE支持 | 有限的代码补全 | 智能代码补全和检查 |
泛型通过容器模式,达到通用性:
// 设计一个通用的容器,但使用时类型安全
public class SafeContainer<T> {
private T value;
public SafeContainer(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
// 通用操作
public <R> R transform(Function<T, R> transformer) {
return transformer.apply(value);
}
}
// 使用:既通用又安全
SafeContainer<String> stringContainer = new SafeContainer<>("Hello");
SafeContainer<Integer> intContainer = new SafeContainer<>(123);
String result = stringContainer.getValue(); // 无需转换
Integer number = intContainer.getValue(); // 无需转换
当然还有很多其他设计模式也可以完成通用性......。
泛型其实是一种补丁?
如果从先后循序来看,泛型更像一种对原始类型的补丁(也可以类比为一种语法糖),在编译器层打了一个补丁,为了弥补类型使用不安全这个问题。
浙公网安备 33010602011771号