泛型程序设计
对于简单的情况,你会发现实现泛型代码很容易。不过,在更高级的情况下,对于实现者来说这会相当复杂。其目标是提供让其他程序员可以轻松使用的类和方法而不出现意外
为什么要使用泛型程序设计
泛型程序设计 generic programming
意味着编写的代码可以对多种不同类型的对象重用。
类型参数的好处
泛型可以使得代码具有更好的可读性
var files = new ArrayList<String>();
如果用一个明确的类型而不是 var 声明一个变量,则可以通过使用"菱形"语法省略构造器中的参数
ArrayList<String> files = new ArrayList<>();
定义简单泛型类
泛型类 generic class
就是有一个或多个类型变量的类,本章使用一个简单的 Pair
类作为例子。
public class Pair<T>
{
private T first;
private T second;
public Pair() { first=null; second = null; }
}
类型变量使用大写字母,而且很简短。Java 库使用变量 E 表示集合的元素类型,K 和 V 分别表示表的键和值的类型
泛型类相当于普通类的工厂
泛型方法
还可以定义一个带有类型参数的方法
class ArrayAlg {
public static <T> T getMiddle(T ... a) {
return a[a.length / 2];
}
}
类型变量放在修饰符 public static
的后面,并在返回类型的前面。泛型方法可以在普通类中定义,也可以在泛型类中定义
在调用泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
在这种情况下,方法调用可以省略 <String>
类型参数编译器有足够的信息推断出你想要的方法。也就是说,可以简单地调用
String middle = ArrayAlg.getMiddle("John", "Q.", "Public")
如果想知道编译器对一个泛型方法调用最终推断出哪种类型,可以故意引入一个错误,然后研究所产生的错误消息
类型变量的限定
有时,类或方法需要对类型变量加以约束。可以通过对类型变量设定一个限定 bound
来实现这一点:
public static <T extends Comparable> T min(T[] a)
此处 <T extends BoundingType>
表示 T
应该是限定类型 bounding type
的子类型 subtype
。 T
和限定类型可以是类,也可以是接口。选择关键字 extends
的原因是它更接近子类型的概念
一个类型变量或通配符可以有多个限定,例如
T extends Comparable & Serializable
在 Java 的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类,如果有一个类作为限定,它必须是限定列表中的第一个限定
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有的对象都属于普通类
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型 raw type
。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除 erased
,并替换为限定类型
比如
public class Pair<T> {
private T first;
private T second;
public T getFirst() {return first;}
}
// 原始类型为
public class Pair {
private Object first;
private Object second;
public Object getFirst() {return first;}
}
因为 T
是一个无限定的变量,所以直接用 Object
替换。
原始类型用第一个限定来替换类型变量,或者,如果没有给限定,就替换为 Object
为了提高效率,应该将标签
tagging
接口(即没有方法的接口)放在限定列表的末尾
转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。例如,对于下面这个语句序列
pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst
擦除类型后的返回类型是 Object
。虚拟机自动插入转换到 Employee
的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法
Pair.getFirst
的调用 - 将返回的
Object
类型强制转换为Employee
类型
转换泛型方法
类型擦除也会出现在泛型方法中。程序员通常认为类似下面的泛型方法
public static <T extend Comparable> T min(T[] a)
是整个一组方法,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
方法的擦除带来了两个复杂问题,看一看下面这个示例:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
// ...
}
}
类型擦除后变为
class DateInterval extends Pair {
public void setSecond(LocalDate second) {
// ...
}
}
此时,还有另外一个从 pair
继承来的 setSecond
方法,即
public void setSecond(Object second)
此时,考虑下面的语句序列:
var interval = new DateInterval();
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
此时我们希望 setSecond
调用具有多态性,会调用最合适的那个方法。问题在于类型擦除与多态产生了冲突。为了解决这个问题,编译器在 DateInterval
类中生成一个桥方法 bridge method
:
public void setSecond(Object second) { setSecond((LocalDate) second); }
在虚拟机中,会由参数类型和返回类型共同指定一个方法,因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况
总之,对于 Java 泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型参数都会替换为它们的限定类型
- 会合成桥方法来保持多态
- 为保持类型安全性,必要时会插入强制类型转换
限制与局限性
大多数限制都是由类型擦除导致的
不能用基本类型实例化类型参数
不能用基本类型代替类型参数。因此,没有 Pair<double>
,只有 Pair<Double>
。原因就在于类型擦除,擦除之后, Pair
类含有 Object
类型的字段,而 Object
不能存储 double
值。
运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询指产生原始类型。比如
if(a instanceof Pair<String>) // ERROR
下面的测试同样如此
if(a instanceof Pair<T>) // ERROR
同样的道理, getClass
方法总是返回原始类型,例如
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() === employeePair.getClass()) // they are equal
不能创建参数化类型的数组
不能实例化参数化类型的数组,例如:
var table = new Pair<String>[10]; // ERROR
如果需要收集参数化类型对象,简单使用
ArrayList:ArrayList<Pair<String>>
更安全、有效