泛型类和泛型方法有类型参数,这使得它们可以准确地描述用特定类型实例化时会发生什么。在有泛型类之前,程序员必须使用Object编写适用于多种类型的代码。这很繁琐,也很不安全。
泛型程序设计(generic programming)意味着编写的代码可以对多种不同类型的对象重用。
定义简单的泛型类
泛型类(generic class)就是有一个或多个类型变量的类。以简单的Pair类作为例子:
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public T getSecond() { return second; }
public void setFirst(T newValue) { first = newValue; }
public void setSecond(T newValue) { second = newValue; }
}
Pair类引入了一个类型变量T,用尖括号(<>)括起来,放在类名的后面。泛型类可以有多个类型变量。例如,可以定义Pair类,其中第一个字段和第二个字段使用不同的类型:
public class Pair<T,U> { ... }
类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。
泛型方法
可以定义一个带有类型参数的方法:
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>类型参数。编译器有足够的信息推断出你想要的方法。它将参数的类型与泛型类型T进行匹配,推断出T一定是String。也就是说,可以简单地调用:
String middle = ArrayAlg.getMiddle("john","Q","public");
类型变量的限定
有时,类或方法需要对类型变量加以约束。
class ArrayAlg {
public static <T> T min(T[] a) {
if (a == null || a.length == 0) return null;
T smallest = a[0];
for (int i = 1;i < a.length;i++) {
if (smallest.compareTo(a[i]) > 0) smallest = a[ii];
}
return smallest;
}
}
但是,这里有一个问题,变量smallest的类型为T,这意味着它可以是任何一个类的对象。如何知道T所属的类有一个compareTo方法呢?
解决这个问题的办法就是限制T只能是实现了comparable接口(包含一个方法compareTo的标准接口)的类。可以通过对类型变量T设置一个限定来实现这一点:
public static <T extends Comparable> T min(T[] a) ...
一个类型变量或通配符可以有多个限定:
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
在Java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
示例:
package section5_5;
public class Pair<T> {
private T first;
private T second;
public Pair() {
first = null;
second = null;
}
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
package section5_5;
public class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) return null;
T min = a[0];
T max = a[0];
for (int i = 1;i < a.length;i++) {
if (min.compareTo(a[i]) > 0) min = a[i];
if (max.compareTo(a[i]) < 0) max = a[i];
}
return new Pair<>(min,max);
}
}
package section5_5;
import java.time.LocalDate;
public class PairTest {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(1906,12,9),
LocalDate.of(1815,12,10),
LocalDate.of(1903,12,3),
LocalDate.of(1910,6,22),
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min="+mm.getFirst());
System.out.println("max="+mm.getSecond());
}
}
泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
例如,Pair<T>的原始类型如下所示:
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() { return first; }
public Object getSecond() { return second; }
public setFirst(Object newValue) { first = newValue; }
public setSecond(Object newValue) { second = newValue; }
}
因为T是一个无限定的变量,所以直接用Object替换。
结果是一个普通的类,就好像引入泛型之前实现的类一样。
转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。例如,对于下面这个语句序列:
Pair<Employee> buddies = ..;
Employee buddy = buddies.getFirst();
getFirst擦除类型后的返回类型是Object。编译器自动插入转换到Employee的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 对原始方法Pair.getFirst的调用。
- 将返回的Object类型强制转换为Employee类型。
当访问一个泛型字段时也要插入强制类型转换。假设Pair类的first字段和second字段都是公共的。表达式
Employee buddy = buddies.first;
也会在结果字节码中插入强制类型转换。
转换泛型方法
类型擦除也会出现在泛型方法中。
public static <T extends Comparable> T min(T[] a)
是整个一组方法,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
注意,类型参数T已经被擦除掉了,只留下限定类型Comparable。
方法的擦除带来了两个复杂问题。如下示例:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) super.setSecond(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(date);
这里,我们希望setSecond调用具有多态性,会调用最合适的那个方法。由于pair引用一个DateInterval对象,所以应该调用DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。为了解决这个问题,编译器在DateInterval类中生成了一个桥方法 (bridge method):
public void setSecond(Object second) { setSecond((LocalDate) second); }
变量pair已经声明为类型Pair<LocalDate>,并且这个类型只有一个名为setSecond的方法,即setSecond(Object)。虚拟机在pair引用的对象上调用这个方法。这个对象是DateInterval类型,因而将会调用DateInterval.setSecond(Object)方法。这个方法是合成的桥方法。它会调用DateInterval.setSecond(LocalDate),这正是我们想要的。
总之,对于Java泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插入强制类型转换。
限制与局限性
不能用基本类型实例化类型参数
没有Pair<double>,只有Pair<Double>。当然,其原因就在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值。
运行时类型查询只适用于原始类型
虚拟机对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。
if (a instanceof Pair<String>) //ERROR
为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,你会得到一个编译器错误(使用instanceof时),或者得到一个警告(使用强制类型转换时)。
同样道理,getClass方法总是返回原始类型。
Pair<String> stringPair = ...;
Pair<Employee> employeePair = ...;
if (stringPair.getClass() == employeePair.getClass())
其比较结果是true,这是因为两次调用getClass调用都返回Pair.class。
不能创建参数化类型的数组
不能实例化参数化类型的数组:
var table = new Pair<String>[10]; //ERROR
Varargs警告
考虑向参数个数可变的方法传递一个泛型类型的实例。
public static <T> void addAll(Collection<T> coll, T... ts) {
for (T t:ts) coll.add(t);
}
实际上参数ts是一个数组,包含提供的所有实参。
Collection<Pair<String>> table = ...;
Pair<String> pair1 = ...;
Pair<String> pair2 = ...;
addAll(table, pair1, pair2);
这样做会得到警告,可以用两种方法来抑制这个警告。一种方法是为包含addAll调用的方法增加注解@SuppressWarnings(“unchecked”)。或者在Java 7中,还可以用@SafeVarargs直接注解addAll方法。
@SafeVarargs只能用于声明为static、final或(Java 9中)private的构造器和方法。所有其他方法都可能被覆盖,使得这个注解没有什么意义。
不能实例化类型变量
不能在类似new T(…)的表达式中使用类型变量。
public Pair() { first = new T(); second = new T(); } //ERROR
类型擦除将T变成Object,而你肯定不希望调用new Object。
在Java 8之后,最好的解决办法是让调用者提供一个构造器表达式。
Pair<String> p = Pair.makePair(String::new);
makePair方法接收一个Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为T的函数:
public static <T> Pair<T> makePair(Supplier constr) {
return new Pair<>(constr.get(), constr.get());
}
比较传统的解决方法是通过反射调用Constructor.newInstance方法来构造泛型对象。
不能调用以下方法:
first = T.class.getConstructor.newInstance(); //ERROR
表达式T.class是不合法的,因为它会擦除为Object.class。
public static <T> Pair<T> makePair(Class<T> cl) {
try {
return new Pair<>(cl.getConstructor().newInstance(),cl.getConstructor().newInstance())
}
catch (Exception e) {
return null;
}
}
注意,Class类本身是泛型的。例如,String.class是一个Class<String>的实例。
不能构造泛型数组
泛型类的静态上下文中类型变量无效
不能抛出或捕获泛型类的实例
泛型类型的继承规则
无论S与T有什么关系,通常Pair<S>与Pair<T>都没有任何关系。
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos); //ERROR
通配符类型
通配符概念
通配符类型
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair<Manager>。
假设要编写一个打印员工对的方法,如下:
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName()+second.getName());
}
不能直接将Pair<Manager>传递给这个方法。可以使用一个通配符类型来解决:
public static void printBuddies(Pair<? extends Employee> p)
类型Pair<Manager>是Pair<? extends Employee>的子类型。
通配符的超类型限定
<? super Manager>
直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
public static <T extends Comparable<T>> T min(T[] a)
上例表示如果计算一个String数组的最小值,T就是类型String,而String是Comparable<String>的一个子类型。
但是,处理一个LocalDate对象的数组时,我们会遇到一个问题。LocalDate实现了ChronoLocalDate,而ChronoLocalDate扩展了Comparable<ChronoLocalDate>。因此LocalDate实现的是Comparable<ChronoLocalDate>而不是Comparable<LocalDate>
在这种情况下,可以利用超类型来解决:
public static <T extends Comparable<? super T>> T min(T[] a) ...