概述
泛型是 Java 1.5 引入的类型机制,将类型参数化,是 1999 年制定的 JSR14 的实现。
泛型主要动机是为了实现类型安全的集合类,以及创建处理抽象类型的新类型。
与泛型相关的类型检查由编译器在编译时进行,具有更好的可读性和安全性,且不需要进行不必要的强制类型转换。
Introduced in J2SE 5.0, this long-awaited enhancement to the type system allows a type or method to operate on objects of various types while providing compile-time type safety. It adds compile-time type safety to the Collections Framework and eliminates the drudgery of casting.
——“The Java™ Tutorials”
通俗意义上的泛型包括:泛型类型(术语称“参数化类型”)和泛型方法(术语称“泛化方法”)。
泛型中的类型是以参数化的方式提供的,即泛型中必然存在“类型变量”,它们被用作参数代表类型,因此称“类型参数”或“形式类型参数”。如:List<T>
中的 T
。
使用泛型时实际传入的类型称为“实际类型”。如:List<String>
中的 String
。
指定了实际类型的泛型类型称为该泛型的“实例化形式”。如:List<String>
泛型有大量相关概念,但其中很多是等价的,比如(正式的术语排在前面):
- 参数化类型/泛型类型
- 泛化方法/泛型方法
- 类型参数/类型变量/形式类型参数
说明
泛型类型
1 | public class GenericClass<T> { |
泛型方法
1 | public static <T> T genericMethod(T arg) { |
通常,泛型方法调用时可以省略类型参数,Java 会进行类型推断。
边界
1 | <T, S extends Comparable & Serializable> |
类型参数总是有一个边界,可以如上代码指定,缺省上界为 Object
。没有下界,因为下界在实际中几乎没有作用。
通配符
类型参数除了可以指定为一般的引用类型外,还可以指定为通配符。
通配符(wildcard)“?”的作用是表示一组类型的集合,可匹配特定范围内的类型。
在使用通配符时可以指定其上界或下界,通过添加上界或下界可以限制通配符表示的具体类型的范围。
编译器在内部使用通配符捕获(wildcard capture)类型的方式来表示通配符代表的类型。
通配符捕获类型,是一种特殊的类型,可以表示通配符所代表的类型集合中的任意类型。
通配符捕获类型与所有具体类型都不兼容,因为一个类型集合无法与单个具体类型兼容。
上界通配符
上界通配符(upper bound wildcard Generics),<? extends SuperClass>
。
只读,通配符匹配的类型只能作为返回值。
? extends Object
等价于无界通配符。
下界通配符
下界通配符(lower bound wildcard Generics),<? super SubClass>
。
只写,通配符匹配的类型可为方法提供参数。
无界通配符
只读。
擦除
泛型仅是编译期特性,Java 中使用类型擦除的方式来实现。
在类型擦除过程中,类型参数会被最左边的上界所替代。
数组
数组对象是由 JVM 根据元素类型创建出来的,重要特征是协变(covariant)。
除了只包含无界通配符的泛型类型和原始类型外,其他泛型类型的实例化形式都不许创建数组,即只有可具体化类型才可创建数组。
关于类型
在 Java 语言中,类型系统描述了不同类型之间的转换关系。
泛型引入对类型系统产生了较大影响,因为泛型类型的实例化形式中包含了所使用的实际类型,它们之间也有父子关系, 相当于把类型系统的一维结构扩展为二维结构。
使用无界通配符的泛型类型实例化形式是所有其他实例化形式的完全父类型。即 List<? extends Number>
是 List<?>
的子类型。
一个泛型类型的所有实例化形式是其对应的原始类型的子类型,即 List<String>
是 List
的子类型。该设计的目的是为了兼容遗留代码。
泛型类型不是协变的,即 List<String>
与 List<Object>
没有父子关系。但 ArrayList<String>
是 List<String>
的子类型。
当一个类型继承自某个泛型类型时,在经过类型擦除后,可能造成所继承的方法签名发生改变。为了保证方法多态性, 编译器会生成桥接方法。在桥接方法的实现中,只是在进行必要的类型转换之后直接调用对应实际类型的方法。
覆盖与重载
覆盖的条件:
- 两个方法 签名相同;
- 父类型方法在类型擦除后的方法签名与子类型方法签名相同。
子类型方法签名中存在类型参数,那么就不可能覆盖父类型的普通方法。
子类型的任何方法都可能覆盖父类型中的泛型方法。
与泛型相关的方法是否存在覆盖关系需要判断:实际类型是否兼容,是否会带来类型安全问题。
类型自动推断方式:
- 根据方法调用时实际参数的静态类型推断;
- 当方法调用的结果被赋值给变量时,根据该变量静态类型推断。
“参数推断”优先。
反射
为了反射 API 的需要,在 Java 字节码中包含了与泛型类型相关的信息,但这些信息在字节代码执行时是不被使用的。
桥接方法在运行时可见,反射 API 可查找并调用桥接方法。判断桥接方法:Method.isBridge()
。
附录
定义
泛型编程(generic programming)
实际使用的类型在代码中只是以参数形式出现的占位符,在具体实例化时,用实际类型替代其中的类型占位符。
参数化类型(parameterized type)
编译器可以自动定制作用于特定类型上的类。Java 中称为范型。
泛型类型(generic type)
使用了形式类型参数的类型。
泛型方法
包含形式类型参数的方法。
类型变量
在类、接口、方法和构造器中用作类型的非限定标识符。
无界通配符(unbounded wildcard)
不包含上界或下界的通配符。
原始类型(raw type)
不指定泛型实际类型而直接使用类型声明所得到的类型。
桥接方法
由编译器自动添加以确保类型擦除后代码实现的正确性的方法。
可具体化类型(reifiable type)
在运行时可用的类型。
规范化
类型变量最好使用有意义的单个大写字母,通常使用 E
表示集合的元素(Element)类型, K
、V
分别代表关键字与值(Key-Value),S
、D
代表原数据和目的数据。 T
表示任意类型(Type),也可以用临近的字母,如:U
和 S
等。
特别说明
编译器把类型参数当成实际类型的占位符。
限制
如果一个泛型方法的类型变量与其所在的泛型类型的类型变量同名,则将被隐藏。
在泛型类型中定义的静态方法和域是被所有实例化形式的对象所共享的,并使用原始类型引用。
同一泛型类型的所有实例化形式在运行时的表示形式是相同的,对应同一份字节码,JVM 不区分。
一个类型不可成为同一泛型类型的两个不同实例化形式的子类型。
不能使用实例化形式的类对象字面量,即不能使用“List<String>.class
”。
除了实际类型都是无界通配符的泛型类型实例化形式外,其他实例化形式,都不能用在 instanceof
操作符中, instanceof
操作符是根据对象的运行时类型来进行判断的,只对可具体化类型有意义,对于泛型类型来说, 只能比较类型擦除之后的类型。
枚举类型、匿名内部类型和异常类型不能添加形式类型参数成为泛型类型。
有界通配符不能同时有上界和下界。
泛型类型声明中的类型参数不能出现在任何静态上下文中,包括:
- 静态域的类型声明
- 静态方法的声明和实现
- 静态初始化代码
- 静态嵌套类型
反常
类型参数的边界可以是 final
类型。final
类不可继承,如 String
,但是泛型类型参数的边界可以是 final
类, 如 <T extends String>
,只是会有警告。
编译器并没有禁止使用不可具体化类型作为长度可变参数的类型,只是给出警告。忽略警告可能产生运行时异常, 除非方法中只读参数内容。
不可具体化类型
除了实际类型都是无界通配符的泛型类型实例化形式外,其他实例化形式都不可具体化。
虚拟机在执行字节码时只能使用运行时可用的可具体化类型,这使 Java 中与虚拟机相关的语法特性对于不可具体化的范型类型不可用。
Java 代码运行时的异常捕获和处理是由虚拟机来完成的,因此异常类型必须是可具体化的,任何泛型类型都不能直接或间接继承自 Throwable
类。
疑难解答
泛型方法类型参数采用前置语法
我们习惯于泛型类型的类型参数后置的语法,比如:List<T>
,但是,泛型方法严格调用时类型参数却是使用的前置语法,如:Classname.<String>genericMethod()
。
这是为了避免歧义。
1 | f<a,b>(c); // 看起来像是一个逗号运算符连接的 2 个逻辑表达式 |
类型参数上界定义关键字是 extends 而不是 implements
<T extends Superclass>
表达式表示:T subtypeOf Superclass
,但不希望引入一个新关键字。
T
即可以是类也可以是接口,从子类型的角度来说,extends
更接近要表达的意思。
严格说来,使用 extends
只是一个约定。
泛型多边界分隔符是 &
分隔符是 &
,而不是逗号,因为逗号已作为多个类型变量的分隔符。如:<T, S extends Comparable & Serializable>
。
编译器禁止在任何静态上下文中使用类型参数
泛型类型的不同实例化形式在运行时对应的是同一个类型,在静态上下文中使用类型参数并没有意义,反而容易造成开发人员的误解。
不能创建泛型数组的原因
数组元素类型信息在运行时是保留的,而泛型类型的类型信息因类型擦除机制而被去掉, 如果允许创建泛型数组,则无法在元素赋值时进行类型检查,因此强制不允许。
注:可创建通配符类型数组,再进行强制类型转换。但类型不安全。
泛型数组引用合法
因为一个非泛型类型可以继承自某个泛型类型,而用它创建数组是合法的。由于数组是协变的, 所以将该数组上转型赋值给其父类型的泛型数组变量也因是合法的。因此,泛型数组引用必须是合法的。
惯用技巧
通配符捕获
可使用 Pair<T>
来捕获 Pair<?>
中的类型。
只允许捕获单个、确定的类型。ArrayList<Pair<?>>
是无法使用 ArrayList<Pair<T>>
捕获的。
实例化实际类型
由于不能用类型参数创建对象,即不能使用 new T()
或 new T[]
。因此,如果需要在泛型方法中创建类型参数的对象, 就需要将类型参数对应的 Class
对象作为参数传入,再通过 Class
对象来创建对象。比如:
1 | static <T> T createGenericObject(Class<T> clazz) throws InstantiationException, IllegalAccessException |
参考
可具体化类型
- 非泛型类型
- 所有实际类型都是无界通配符的参数化类型
- 原始类型
- 基本类型
- 元素类型为可具体化类型的数组类型
- 外围类型和自身都是可具体化类型的嵌套类型
泛型转换的事实
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的实际类型替换。
- 桥接方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
对比
类型参数 vs. 普通类型
相同点(适用):
- 方法参数、返回值类型
- 域和局部变量的类型声明
- 强制类型转换及泛型类型和泛型方法的实际类型参数
不同点(类型参数不适用):
- 创建对象和数组(
new T()
、new T[]
) - 父类型(
class MyClass extends T
) - 使用在
instanceof
表达式中(instanceof T
) - 类型字面量(
T.class
) - 异常处理中(
catch(T)
) - 静态上下文中(
static T
)
常见问题
方法重载错误
编译错误信息为:Erasure of method xyz(…) is the same as another method in type Abc。
1 | public void proscess(List<Employee> employees) { |
上面的两个方法,同名但参数列表不同,看起来像是重载。但是,这仅仅是源代码级别的表象,由于它们的参数包含泛型,编译后泛型将被擦除,结果是参数列表也相同。
因此,存在两个签名相同的方法,这是不合法的。解决方案就是分别为它们声明不同的方法名,就不会导致这样的“伪重载”。
协变错误
编译错误信息为:The method xyz(Foo) in the type Abc is not applicable for the arguments (Foo)。
1 | public void process(List<Number> list) { |
问题的关键在于泛型不是协变的。
但也许有人会疑惑,擦除后类型将是一样的,那么编译器应该允许这种情况才对。事实上,编译器知道,这不是合适替代,如果允许编译会导致运行时问题。
解决方案,一是分别声明两个不同的方法,或者使用泛型上界。
原始类型警告
编译警告信息为:Foo is a raw type.References to generic type Foo should be parameterized。
当将一个泛型类型的对象赋值给一个原始类型变量时,会出现这个警告。因为,这可能引发一些问题。
假设有如下一个泛型类:
1 | public class GenericType<T> |
编译后,代码中的 T
都会被替换回 Object
。
再编写一个子类,如下:
1 | class SubGenericType extends GenericType<String> { |
注意,我们重写了 setObj() 方法,但事实并非如此。
为了保留多态性,编译器会为子类生成 setObj() 桥接方法的重写版本。因此,子类实际看起来应该是这样:
1 | ... |
现在运行以下代码:
1 | SubGenericType sgt = new SubGenericType("test"); |
结果将得到一个类型转换异常: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
这就是所谓的堆污染。
使用泛型的最佳实践是:坚决不让泛型相关警告悄然存在。编译器会提示未正确使用泛型,应认真留意这些警告。
示例代码
1 | /** |