Java基础知识之泛型程序设计
本文内容参考《Java核心技术 卷Ⅰ》
定义简单泛型
1 | class Pair<T> { |
Pair
类引入了一个类型变量T
,用尖括号括起来,放在类名后面。泛型可以有多个类型变量
1 | class Pair<T, U> {...} |
泛型相当于普通类的工厂。
泛型方法
1 | class ArrayAlg { |
这是一个泛型方法,类型变量放在修饰符的后面,并在返回类型的前面。
泛型方法可以在普通类中定义,也可以在泛型类中定义。
调用泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面,也可以直接省略
1 | String middle = ArrayAlg.<String>getMiddle("a", "b", "c"); // OK |
变量类型的限定
有时,类或方法需要对类型变量加以约束。假如我们要计算数组中的最小元素
1 | class ArrayAlg { |
因为smallest
可能是任意一种类型,我们不能知道它是否有compareTo
方法。为了解决这个问题,可以对类型变量T
设置一个限定
1 | public static <T extends Comparable> T min(T[] a) ... |
一个变量类型或通配符可以有多个定义
1 | T extends Comparable & Serializable |
限定类型用&
来分隔,类型变量用,
来分隔。
泛型代码和虚拟机
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并被替换为其限定的类型。
Pair<T>
的原始类型如下
1 | class Pair { |
原始类型用第一个限定来替换类型变量,如果没有限定,直接替换为Object
。
1 | class Interval<T extends Comparable & Serializable> implements Serializable { |
原始类型Interval
如下
1 | class Interval implements Serializable { |
转换泛型表达式
如果擦除了泛型方法的返回类型,编译器会插入强制类型转换
1 | Pair<Employee> buddies = ...; |
getFirst
擦除类型后返回类型是Object
。编译器自动插入转换到Employee
的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法
Pair.getFirst
的调用。 - 将返回的
Object
类型强制转换为Employee
类型。
转换泛型方法
类型擦除也会出现在类型方法中。
1 | public static <T extends Comparable> T min(T[] a) |
该方法类型擦除后会变成
1 | public static Comparable min(Comparable[] a) |
如果类型擦除与多态发生了冲突,编译器会在类中生成一个桥方法。
对于Java泛型转换,需要记住:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插入强制类型转换。
限制与局限性
不能用基本类型实例化类型参数
没有Pair<double>
,只有Pair<Double>
。因为有类型擦除,Object
不能存储double
值。
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
1 | if (a instanceof Pair<String>) // ERROR |
同理,getClass
方法总返回原始类型
1 | Pair<String> stringPair = ...; |
不能创建参数化类型的数组
1 | Pair<String>[] table = new Pair<String>[10]; // ERROR |
因为在类型擦除之后,table
的类型是Pair[]
,可以把它转换为Object[]
1 | Object[] objarray = table; |
数组会记住它的元素类型,如果试图存储其它类型的元素,就会抛出一个ArrayStoreException
异常。但是对于泛型来说,擦除会使这种机制无效。只不过声明类型为Pair<String>[]
的变量仍是合法的,只是不能用new Pair<String>[10]
初始化变量。
不能实例化类型变量
不能在类似new T(...)
的表达式中使用类型变量。
1 | public Pair() { |
类型擦除将使T
变成Object
。
最好的解决办法是让调用者提供一个构造器表达式
1 | Pair<String> p = Pair.makePair(String::new); |
makePair
方法接收一个Supplier<T>
,这是一个函数式接口,表示一个无参数而且返回类型为T
的函数
1 | public static <T> Pair<T> makePair(Supplier<T> constr) { |
比较传统的解决办法是通过反射调用Constructor.newInstance
方法来构造泛型对象。
不能构造泛型数组
1 | public static <T extends Comparable> T[] minmax(T... a) { |
类型擦除会让这个方法总是构造Comparable[2]
的数组。
如果数组仅作为一个类的私有实例字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换。
1 | class ArrayAlg { |
以上代码在编译时不会有任何警告,当方法返回后Comparable[]
引用强制转换为String[]
时,将会出现ClassCastException
异常。
可以让用户提供一个数组构造器表达式
1 | class ArrayAlg { |
另一种方法是利用反射,并调用Array.newInstance
1 | public static <T extends Comparable> T[] minmax(T... a) { |
泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。
1 | class Singleton<T> { |
通配符类型
通配符概念
在通配符类型中,允许类型参数发生变化。Pair<? extends Employee>
表示任何泛型Pair
类型,它的类型参数时Employee
的子类。
通配符的超类型限定
通配符限定可以指定一个超类型限定,例如? super Manager
,这个通配符限制为Manager
的所有超类型。
带超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读入一个泛型对象。
无限定通配符
假设我们要测试一个对组是否包含null引用,它不需要实际的类型
1 | public static boolean hasNulls(Pair<?> p) { |
通过将hasNulls转换成泛型类型,避免使用通配符类型。