什么是泛型
具有参数化类型的类、接口或方法。具体的类型在运行时才确定。
在泛型出现前通过使用 Object 引用也可以达到泛型的效果,但是缺乏类型安全检查,泛型添加了这一点。
简单的泛型例子
// T 是类型参数,作为实际类型的占位符
class Gen<T> {
T v;
Gen(T o) {
v = o;
}
T getV() {
return v;
}
}
class Demo {
public static void main(String[] args) {
Gen<Integer> i = new Gen<Integer>(8);
int v = i.getV();
}
}
JDK10 开始,不能使用 var 作为类型参数。任意合法的标识符都可以作为类型参数,但默认使用大写单字母,如 T/V/E。
上述代码中,在创建泛型对象时,编译器并没有创建对应版本的 Gen 类,而是移除了所有的泛型信息,使用类型转换使得代码行为仿佛是根据传入的类型创建了一个特定版本的 Gen 类。这个过程称为类型擦除(erasure)。
当把 Gen<Double> 类型的实例赋给 Gen<Integer> 类型的引用时,会发生编译错误。为了防止错误,相同泛型类的不同版本是不兼容的。
传递给类型参数的类型必须是引用类型。
如果不使用泛型,使用 Object 类则在得到类中的值时必须进行类型转换,同时不当的类型转换只有在运行时才能被检查出来。而使用泛型不用显式类型转换,这种错误在编译时能被检查出来。
有两参数的泛型类
class TwoGen<T, V> {
T o1;
V o2;
TwoGen(T t, V v) {
o1 = t;
o2 = v;
}
T getO1() {
return o1;
}
V getO2() {
return o2;
}
}
class Sim {
public static void main(string[] args) {
TwoGen<String, Integer> tg = new TwoGen<>("Name", 10);
}
}
参数类型可以为多个。
限制类型
<T extends superClass>
:替换 T 的实际类型必须为超类或超类的任意子类。
可以同时限定一个类和多个接口,替换 T 的类型必须是该类或该类的子类,同时需要实现指定的接口。如
// & 用于分隔类和接口,类必须位于第一个
<T extends ClassName & InterfaceName1>
通配符参数
<?>
:通配符,表示未知类型。仅匹配类型参数允许的类型。
<? extends ClassName>
:表示该通配符只能匹配 ClassName 类及其子类。
<? super subClass>
:表示该通配符只能匹配 subClass 类及其超类。
泛型方法
非泛型类中可以包含泛型方法。一般形式为:
<typeParamList> returnType methodName(paramList) { }
在泛型方法中,类型参数在返回类型前面定义。泛型方法可以是静态,也可以是非静态。
// Comparable 是一个泛型接口,实现了该接口的类是有序的,即可以比较
<T extends Comparable<T>, V extends T> boolean methodName(T a, V b)
调用方法时可以显式指定类型参数,如 className.<Integer, String>methodName(param)
。
非泛型类中可以使用泛型构造器。
泛型接口
// 定义接口
interface IName<T extends Comparable<T>> {}
// 实现接口,为保证接口的类型参数符合,定义与接口一致,同时直接将类型参数传给接口就行
class CName<T extends Comparable<T>> implements IName<T> {}
实现泛型接口的类如果不是泛型类,需要传递具体类型给接口。否则,实现类必须为泛型类。
// 错误
class Cname implements IName<T> {}
// 正确
class Cname implements IName<Integer> {}
泛型接口可以有不同的数据类型实现;也可以限制实现该接口的数据类型。
raw types 和遗留代码
在泛型出现前,通常使用 Object 代替 T 达到泛型的效果。为了保证没有使用泛型的代码和新的泛型代码一起工作,需要使用 raw types。raw types 就是在创建泛型实例时并不传入实际类型替换 T,此时 T 默认为 Object,与泛型出现之前的方式一致。但这种方法会损失泛型的安全性检查,如果出现类型相关错误,只会在运行时发生,编译时检查不出来。老代码中尽量少用,新代码中不用。
泛型类层次
在类继承层次中,泛型类和非泛型类基本相似,最大的区别是,在泛型类中,超类需要的类型参数需要子类传递给它,即使子类并没有用到类型参数。具体的传递通过构造器。
class Super<T> {
Super(T o) {}
}
// 子类定义 T 是因为超类需要,通过构造器传递,子类并不需要类型参数。
class Sub<T> extends Super<T> {
Sub(T o) {
super(o);
}
}
// 子类定义了两个类型参数,一个自用,另一个传递给超类。
class Ano<T, V> extends Super<T> {}
非泛型类可以作为泛型类的超类,此时子类不需要传递类型参数给超类。
使用 instanceof 判断一个实例的类型是否为某个泛型类或其子类时,泛型类使用通配符。不能使用特定的类型,因为在运行时,类型信息不可获得。
// 判断 obj 对象的类型是否为泛型类 GenName 或者其子类
obj instanceof GenName<?>
// 错误,不能使用具体类型,运行时类型信息不可获得
obj instanceof GenName<Integer>
泛型类的类型转换仅当实际类型一致时子类可以转换成超类。如 Sub<Integer>
类型的 obj 可以转成 Sup<Integer>
类型 (Sup<Integer>)obj
;但不能转成 Sup<String>
类型。
在泛型类继承层次中子类重写超类的方法和非泛型类一致。
泛型类型推断
从 JDK7 开始,提供了简写创建泛型类实例的方法,也可用于方法调用中。
// 具体的类型根据参数调用相应的构造器决定
GenName<Integer> a = new GenName<>(10);
// 方法调用
m(new GenName<>(10));
局部变量类型推导与泛型
// 另一种简写方式
var a = new GenName<Integer>(10)
擦除
Java 语法或者 JVM 的改变必须和泛型之前的老代码兼容,Java 使用了擦除达到这个要求。
Java 源码在编译时,编译器将所有关于泛型类型的信息去除(即擦除),如果泛型是限制类型,使用该限制类型替换类型参数;如果泛型不是限制类型,使用 Object 替换类型参数。例如 <T extends Number>
、<T>
在擦除时分别使用 Number、Object 替换 T。然后使用合适的类型转换保持指定类型的兼容性。这个机制意味着泛型仅在源代码阶段有用,在运行时类型参数不存在。
在泛型类继承层次中,子类重写了超类中的方法,但经过擦除,子类和超类中的方法可能不满足重写的条件。这时编译器会自动添加一个桥接方法(bridge method)使得子类满足重写条件。桥接方法是在字节码层面生成的,不能看也不能显式调用。
class Gen<T> {
T a;
T get() {
return a;
}
}
/*
即使子类已指定超类类型为 String,但是经过擦除之后类型变为 Object,
超类中被重写的方法变为:Object get(), 与子类中的 String get() 返回
类型不一致,是两个不同的方法。此时,编译器在子类中创建桥接方法,伪码为
Object get(),在其中调用 String get() 达到重写超类方法的目的。
*/
class Sub extends Gen<String> {
String get() {
return a;
}
}
二义性错误
两个表面上不同的泛型类定义在擦除之后可能是相同的,造成冲突。比如使用不同的类型参数重载方法,如 T get()
和 V get()
在擦除后,都变成了 Object get()
,造成冲突。
泛型限制
不能实例化类型参数。如 new T()
,因为此时 T 作为占位符,编译器不知道具体类型。
不能在类中定义静态类型参数成员,如 static T a
和 static T get()
,但可以定义静态泛型方法。
不能实例化将类型参数作为类型的数组,如 new T[5];
,因为此时编译器无法知道数组的具体类型;不能定义特定泛型类型的数组引用,如 Gen<Integer> a[] = new Gen<Integer>[12];
。但是可以定义泛型的数组引用,如 Gen<?> a[] = new Gen<?>[5];
。
泛型类不能继承 Throwable
,也就是不能创建泛型异常类。
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.
[2] https://wiyi.org/bridge-method-in-java.html