文章目录
泛型程序设计
8.2定义简单泛型类
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 first) {
this.first = first;
}
public void setSecond(T second) {
this.second = second;
}
}
常见的做法是类型变量使用大写字母,而且很简短。Java库使用变量
E
表示集合的元素类型,K
和V
分别表示表的键和值的类型。T
(必要时还可以用相邻的字母U
和S
)表示任意类型。
8.3泛型方法
类型变量放在修饰符的后面,并在返回类型的前面。泛型方法可以在普通类中定义,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
class ArrayAlg {
public static <T> T getMiddle(T... a) {
return a[a.length / 2];
}
}
String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");
在这种情况下(实际也是大多数情况下),方法调用中可以省略类型参数。编译器有足够的信息推断出想要的方法。它将参数的类型与泛型类型进行匹配。几乎在所有情况下,泛型方法的类型推导都能正常工作。偶尔,编译器也会提示错误,此时就需要解析错误报告。例如:
double middle = ArrayAlg.getMiddle(3.14, 1729, 0);
// 解析这个代码有两种方式,而且这两种方式都是合法的。简单地说,编译器将把参数自动装箱为
// 1个Double和2个Integer对象,然后寻找这些类的共同超类型。事实上,它找到了2个超类型:
// Number和Comparable接口。在这种情况下,可以采取的补救措施是将所有的参数都写为double值。
8.4类型变量的限定
有时,类或方法需要对类型变量加以约束:
public static <T extends Comparable> T min(T[] a) { /* ... */ }
一个类型变量或通配符可以有多个限定:
T extends Comparable & Serializable
。如果有一个类作为限定,它必须是限定列表中的第一个限定。
8.5泛型代码和虚拟机
虚拟机没有泛型类型对象——所有对象都属于普通类。
8.5.1类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除,并替换为其第一个限定类型,因此,为了提高效率,应该将标签接口(即没有方法的接口)放在限定列表的末尾(或者,对于无限定的变量则替换为
Object
)。
需要注意的是,java泛型与c++模板有很大的区别。C++会为每个模板的实例化产生不同的类型,java不存在这个问题的困扰。
8.5.2转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换(访问一个泛型字段时也是如此)。
8.5.3转换泛型方法
类型擦除也会出现在泛型方法中。通常认为类似下面的泛型方法:
public static <T extends Comparable> T min(T[] a);
是整个一组方法,而擦除类型后,只剩下一个方法:
public static Comparable min(Comparable[] a);
方法的擦除带来了两个复杂问题:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0) {
super.setSecond(second);
}
}
// ...
}
这个类擦除后变成:
class DateInterval extends Pair { // After erasure
public void setSecond(LocalDate second) { /* ... */ }
}
令人感到奇怪的是,还有另一个从
Pair
继承的setSecond
方法,即:
public void setSecond(Object second);
这显然是一个不同的方法,因为它有一个不同类型的参数——
Object
,而不是LocalDate
。不过,不应该不一样。考虑下面的语句序列:
var interval = new DateInterval(...);
Pair<LocalDate> pair = interval; // OK--assignment to superclass
pair.setSecond(aDate);
这里,希望
setSecond
调用具有多态性,会调用最合适的那个方法。由于pair
引用一个DateInterval
对象,所以应该调用DateInterval.setSecond
。问题在于类型擦除与多态发生了冲突。为了解决这个问题,编译器在DateInerval
类中生成了一个桥方法(如果没有桥方法,会认为继承的方法没有被重写,因此不能实现多态):
public void setSecond(Object second) { setSecond((LocalDate) second); }
要想了解为什么这样可行,请仔细跟踪以下语句的执行:
pair.setSecond(aDate)
。变量pair
已经声明为类型Pair<LocalDate>
,并且这个类型只有一个名为setSecond
方法,即setSecond(Object)
。虚拟机在pair
引用的对象上调用这个方法。这个对象是DateInterval
类型,因而将会调用DateInterval.setSecond(Object)
方法。这个方法是合成的桥方法。它会调用DateInterval.setSecond(LocalDate)
,这是想要的。
桥方法可能会变得更奇怪。假设DateInterval
类也覆盖了getSecond
方法:
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (LocalDate) super.getSecond();
}
// ...
}
在
DateInterval
类中,有两个getSecond
方法:
LocalDate getSecond(); // Defined in DateInterval
Object getSecond(); // Overrides the method defined in Pair to call the first method
不能这样编写java代码(两个同名方法有相同的参数类型是不合法的)。但是在虚拟机中,会由参数类型和返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况。
桥方法不仅适用于泛型类型。一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,这是合法的。例如:
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException { /* ... */ }
}
Object.clone
和Employee.clone
方法被称为有协变的返回类型。实际上,Employee
类有两个克隆方法:
Employee clone(); // Defined above
Object clone(); // Synthesized bridge method, overrides Object.clone
合成的桥方法会调用新定义的方法。
总之,对于java泛型的转换,需要记住以下几个事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插入强制类型转换。
8.6限制与局限性
大多数限制都是由类型擦除引起的。
8.6.1不能用基本类型实例化类型参数
不能用基本类型代替类型参数。因此,没有
Pair<double>
,只有Pair<Double>
。当然,其原因就在于类型擦除。擦除之后,Pair
类含有Object
类型的字段,而Object
不能存储double
值。
8.6.2运行时类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型:
if (a instanceof Pair<String>) // ERROR
if (a instanceof Pair<T>) // ERROR
Pair<String> p = (Pair<String>) a; // Warning--can only test that a is a Pair
为提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,会得到一个编译器错误(使用
instanceof
时),或者得到一个警告(使用强制类型转换时)。
同样的道理,getClass
方法总是返回原始类型。
8.6.3不能创建参数化类型的数组
不能实例化参数化类型的数组。例如:
var table = new Pair<String>[10]; // ERROR
问题在于,类型擦除之后,
table
的类型是Pair[]
。可以把它转换为Object[]
:
Object[] objarray = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个
ArrayStoreException
异常:
objArray[0] = "Hello"; // Error--component type is Pair
不过对于泛型类型,擦除会使这种机制无效:
objArray[0] = new Pair<Employee>();
尽管能够通过数组存储的检查,但仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。
需要说明的是,只是不允许创建这些数组,而声明类型为Pair<String>[]
的变量仍是合法的。不过不能用new Pair<String>[10]
初始化这个变量。
可以声明通配类型的数组,然后进行强制类型转换:
var table = (Pair<String>[]) new Pair<?>[10];
结果将是不安全的。如果在
table[0]
中存储一个Pair<Employee>
,然后对table[0].getFirst()
调用一个String
方法,会得到一个ClassCastException
异常。
如果需要收集参数化类型对象,简单地使用ArrayList:ArrayList<Pair<String>>
更安全有效。
8.6.5不能实例化类型变量
不能在类似
new T(...)
的表达式中使用类型变量。例如:
public Pair() {
first = new T(); // ERROR
second = new T();
}
类型擦除将
T
变成Object
,而肯定不希望调用new Object()
。
在java8之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
public static <T> Pair<T> makePair(Supplier<T> constr) {
return new Pair<>(constr.get(), constr.get());
}
Pair<String> p = makePair(String::new);
比较传统的解决办法是通过反射调用
Constructor.newInstance
方法来构造泛型对象。遗憾的是,细节有点复杂。不能调用以下方法:
first = T.class.getConstructor().newInstance();
表达式
T.class
是不合法的,因为它会擦除为Object.class
。必须适当地设计API以便得到一个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;
}
}
Pair<String> p = makePair(String.class);
原因在于,
Class
类本身是泛型的。
8.6.6不能构造泛型数组
就像不能实例化泛型实例一样,也不能实例化泛型数组。不过原因有所不同,毕竟数组可以填充
null
值,看上去好像可以安全地构造。不过,数组本身也带有类型,用来监控虚拟机中的数组存储。这个类型会被擦除。例如:
public static <T extends Comparable> T[] minmax(T... a) {
T[] mm = new T[2]; // ERROR
// ...
}
类型擦除会让这个方法总是构造
Comparable[2]
数组。
如果数组仅仅作为一个类的私有字段,那么可以将这个数组的元素类型声明为擦除的类型并使用强制类型转换。例如,ArrayList
类可以如下实现:
public class ArrayList<E> {
private Object[] elements;
// ...
@SuppressWarnings("unchecked")
public E get(int n) {
return (E) elements[n];
}
public void set(int n, E e) {
elements[n] = e; // No cast needed
}
}
但实际的实现没有这么清晰:
public class ArrayList<E> {
private E[] elements;
// ...
public ArrayList() {
elements = (E[]) new Object[10];
}
}
这里,强制类型转换
E[]
是一个假象,而类型擦除使其无法察觉。
这个技术并不适用于minmax
方法,因为minmax
方法返回一个T[]
数组,如果类型不对,就会得到运行时错误结果。假设实现以下代码:
public static <T extends Comparable> T[] minmax(T... a) {
var result = new Comparable[2]; // Array of erased type
// ...
return (T[]) result; // Compiles with warning
}
以下调用:
String[] names = ArrayAlg.minmax("Tom", "Dick", "Harry");
编译时不会有任何警告。当方法返回后
Comparable[]
引用强制转换为String[]
时,将会出现ClassCastException
异常。
在这种情况下,最好让用户提供一个数组构造器表达式:
public static <T extends Comparable> T[] minmax(IntFunction<T[]> constr, T... a) {
T[] result = constr.apply(2);
// ...
}
String[] names = ArrayAlg.minmax(String[]::new, "Tom", "Dick", "Harry");
比较老式的方法是利用反射,并调用
Array.newInstance
:
public static <T extends Comparable> T[] minmax(T... a) {
var result = (T[]) Array.newInstance(a.getClass().getComponentType(), 2);
// ...
}
ArrayList
类的toArray
方法就没有这么幸运。它需要生成一个T[]
数组,但没有元素类型。因此,有下面两种不同的形式:
Object[] toArray();
T[] toArray(T[] result);
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用
result
的元素类型构造一个足够大的新数组。
8.6.7泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量:
public class Singleton<T> {
private static T singleInstance; // ERROR
public static T getSingleInstance() { // ERROR
if (singleInstance == null) {
// Construct new instance of T
}
return singleInstance;
}
}
如果这样可行,程序就可以声明一个
Singleton<Random>
共享一个随机数生成器,另外声明一个Singleton<JFileChooser>
共享一个文件选择器对话框。但是,这样是行不通的。类型擦除之后,只剩下Singleton
类,它只包含一个singleInstance
字段。因此,禁止使用带有类型变量的静态字段和方法。
8.6.8不能抛出或捕获泛型类的实例
既不能抛出也不能捕获泛型类的对象。实际上,泛型类扩展
Throwable
甚至是不合法的。例如:
public class Problem<T> extends Exception { /* ... */ }
// ERROR--can't extend Throwable
catch
子句中不能使用类型变量。例如:
public static <T extends Throwable> void doWork(Class<T> t) {
try {
// Do work
} catch (T e) { // ERROR--can't catch type variable
Logger.global.info(...);
}
}
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
public static <T extends Throwable> void doWork(T t) throws Throwable {
try {
// Do work
} catch (Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
8.6.9可以取消对检查型异常的检查
Java异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。关键在于以下方法:
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
假设这个方法包含在接口
Task
中,如果有一个检查型异常e
,并调用:
Task.<RuntimeException>throwAs(e);
编译器就会认为
e
是一个非检查型异常。以下代码会把所有异常都转换为编译器所认为的非检查型异常:
try {
// Do work
} catch (Throwable t) {
Task.<RuntimeException>throwAs(t);
}
下面使用这个技术解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了
Runnable
接口的类的run
方法中。不过这个方法不允许抛出检查型异常。将提供一个从Task
到Runnable
的适配器,它的run
方法可以抛出任意异常:
interface Task {
// 注意这里抛出了Exception
void run() throws Exception;
@SuppressWarnings("unchecked")
static <T extends Throwable> void throwAs(Throwable t) throws T {
throw (T) t;
}
static Runnable asRunnable(Task task) {
return () -> {
try {
task.run();
} catch(Exception e) {
Task.<RuntimeException>throwAs(e);
}
};
}
}
// 以下程序运行了一个线程,它会抛出一个检查型异常:
public class Test {
public static void main(String[] args) {
var thread = new Thread(Task.asRunnable(() -> {
// 由于Task.run抛出了Exception,因此这里不需要捕获异常。
Thread.sleep(1000);
System.out.println("Hello, world!");
// 抛出检查型异常
throw new Exception("Check this out!");
}));
thread.start();
}
}
Thread.sleep
方法声明为抛出一个InterruptedException
,不再需要捕获这个异常。由于没有中断这个线程,这个异常不会被抛出。不过,程序会抛出一个检查型异常。运行程序时,会得到一个堆栈轨迹。
正常情况下,必须捕获一个Runnable
的run
方法中的所有检查型异常,把它们包装到非检查型异常中,因为run
方法声明为不抛出任何检查型异常。
不过在这里并没有做这种包装。只是抛出异常,并哄骗编译器,让它相信这不是一个检查型异常。
通过使用泛型类、擦除和@SuppressWarnings
注解,就能消除java类型系统的部分基本限制。
8.6.10注意擦除后的冲突
当泛型类型被擦除后,不允许创建引发冲突的条件。例如:
public class Pair<T> {
public boolean equals(T value) {
return first.equals(value) && second.equals(value);
}
}
考虑一个
Pair<String>
。从概念上讲,它有两个equals
方法:
boolean equals(String); // Defined in Pair<T>
boolean equals(Object); // Inherited from Object
但是,直觉把我们引入歧途。方法:
boolean equals(T);
擦除后就是:
boolean equals(Object);
这会与
Object.equals
方法发生冲突(返回值和参数类型都一样)。当然,补救的办法是重新命名引发冲突的方法。
泛型规范说明还引用了另外一个原则:为了支持擦除转换,要施加一个限制,倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。例如:
class Employee implements Comparable<Employee> { /* ... */ }
// Manager实现了Comparable<Employee>和Comparable<Manager>,这是同一接口的不同参数化。
class Manager extends Employee implements Comparable<Manager> { /* ... */ } // ERROR
这一限制与类型擦除的关系并不十分明显。毕竟,非泛型版本是合法的。其原因非常微妙,有可能与合成的桥方法产生冲突。实现了
Comparable<X>
的类会获得一个桥方法:
public int compareTo(Object other) {
return compareTo((X) other);
}
不能对不同的类型
X
有两个这样的方法。
8.7泛型类型的继承规则
在使用泛型类时,需要了解有关继承和子类型的一些规则。考虑一个类和一个子类,如
Employee
和Manager
,但是Pair<Manager>
并不是Pair<Employee>
的一个子类。因此,下面的代码将不能成功编译:
Manager[] topHonchos = ...;
Pair<Employee> result = ArrayAlg.minmax(topHonchos); // ERROR
// minmax返回Pair<Manager>,而不是Pair<Employee>,
// 并且这样的赋值是不合法的。
无论
S
与T
有什么关系,通常,Pair<S>
与Pair<T>
都没有任何关系。
这看起来是一个很严格的限制,不过对于类型安全非常必要。假设允许将
Pair<Manager>
转换为Pair<Employee>
。考虑下面代码:
var managerBuddies = new Pair<Manager>(ceo, cfo);
Pair<Employee> employeeBuddies = managerBuddies; // Illegal, but suppose it wasn't
employeeBuddies.setFirst(lowlyEmployee);
显然,最后一句是合法的。但是
employeeBuddies
和managerBuddies
引用了同样的对象。现在我们会把CFO和一个普通员工组成一对,这对于Pair<Manager>
来说应该是不可能的。
总是可以将参数化类型转换为一个原始类型。例如,Pair<Employee>
是原始类型Pair
的一个子类型。在与遗留代码交互时,这个转换非常重要(但是在这个过程中会失去泛型程序设计提供的附加安全性):
var managerBuddies = new Pair<Manager>(ceo, cfo);
Pair rawBuddies = managerBuddies; // OK
rawBuddies.setFirst(new File("...")); // Only a compile-time warning
最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,它们与普通的类没有什么区别。
8.8通配符类型
8.8.1通配符概念
在通配符类型中,允许类型参数发生变化。例如,通配符类型:
Pair<? extends Employee>
表示任何泛型Pair
类型,它的类型参数是Employee
的子类,如Pair<Manager>
,但不是Pair<String>
。例如:
public static void printBuddies(Pair<Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
// 不能将Pair<Manager>传递给这个方法,这一点很有限制。
// 不过解决的方法很简单,可以使用一个通配符类型:
public static void printBuddies(Pair<? extends Employee> p) { /* ... */ }
类型
Pair<Manager>
是Pair<? extends Employee>
的子类型。
并且使用通配符并不会通过
Pair<? extends Employee>
的引用破坏Pair<Manager>
:
var managerBuddies = new Pair<Manager>(ceo, cfo);
Pair<? extends Employee> wildcardBuddies = managerBuddies; // OK
wildcardBuddies.setFirst(lowlyEmployee); // Compile-time error
这不可能引起破坏。对
setFirst
的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型Pair<? extends Employee>
。它的方法如下:
? extends Employee getFirst();
void setFirst(? extends Employee);
这样将不能调用
setFirst
方法。编译器只知道需要Employee
的某个子类型,但不知道具体是什么类型。它拒绝传递任何特定的类型。毕竟?
不能匹配。
使用getFirst
就不存在这个问题:将getFirst
的返回值赋给一个Employee
引用是完全合法的。
8.8.2通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定:
? super Manager
。这个通配符限制为Manager
的所有超类型。带有超类型限定的通配符可以为方法提供参数,但不能使用返回值。例如:
void setFirst(? super Manager);
? super Manager getFirst();
编译器无法知道
setFirst
方法的具体类型,因此不能接受参数类型为Employee
或Object
的方法调用。只能传递Manager
或者其某个子类型对象(最低都是Manager
,那么传递它或者它的子类型对象都是可以的)。另外,如果调用getFirst
,不能保证返回对象的类型,只能把它赋给一个Object
(只知道里面存储的是Manager
或者其基类,具体是哪个不知道,所以只能赋给Object
)。
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
if (a.length == 0) {
return;
}
Manager min = a[0];
Manager max = a[0];
for (int i = 1; i < a.length; i++) {
if (min.getBonus() > a[i].getBonus()) {
min = a[i];
}
if (max.getBonus() < a[i].getBonus()) {
max = a[i];
}
}
result.setFirst(min);
result.setSecond(max);
}
直观地讲,带有超类型限定的通配符允许写入一个泛型对象,而带有子类型限定的通配符允许读取一个泛型对象。
// 注意这里的Comparable<T>
public static <T extends Comparable<T>> T min(T[] a) { /* ... */ }
public static void main(String[] args) {
LocalDate[] localDates = new LocalDate[4];
LocalDate min = min(localDates); // Error:required type is LocalDate, but provide ChronoLocalDate
}
看起来,这样写比只使用
T extends Comparable
更彻底,并且对许多类来讲,这样工作得更好。但是,处理一个LocalDate
对象的数组时,会遇到一个问题。LocalDate
实现了ChronoLocalDate
,而ChronoLocalDate
扩展了Comparable<ChronoLocalDate>
。因此,LocalDate
实现的是Comparable<ChronoLocalDate>
而不是Comparable<LocalDate>
。
在这种情况下,可以利用超类型来解决:
public static <T extends Comparable<? super T>> T min(T[] a) {/* ... */}
现在
compareTo
方法写成:int compareTo(? super T)
。它可以声明为使用类型T
的对象,或者也可以是使用T
的一个超类型的对象(当T
是LocalDate
时)。无论如何,向compareTo
方法传递一个T
类型的对象是安全的。
子类型限定的另一个常见的用法是作为一个函数式接口的参数类型。例如,Collection
接口有一个方法:
default boolean removeIf(Predicate<? super E> filter);
这个方法会删除所有满足给定谓词条件的元素。例如,如果不喜欢有奇怪散列码的员工,就可以如下将他们删除:
ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);
// 希望能够传入一个Predicate<Object>,而不只是Predicate<Employee>。
// super通配符可以使这个愿望成真。
8.8.3无限定通配符
还可以使用根本无限定的通配符。例如,
Pair<?>
。初看起来,这好像与原始的Pair
类型一样。实际上,这两种类型有很大的不同。类型Pair<?>
有以下方法:
? getFirst();
void setFirst(?);
getFirst
的返回值只能赋给一个Object
。setFirst
方法不能被调用,甚至不能用Object
调用,但是可以调用setFirst(null)
。Pair<?>
和Pair
本质的不同在于:可以用任意Object
对象调用原始Pair
类的setFirst
方法。
虽然很脆弱,但是对于很多简单操作非常有用。例如:
// 测试是否包含一个null引用,它不需要实际的类型
public static boolean hasNulls(Pair<?> p) {
return p.getFirst() == null || p.getSecond() == null;
}
// 通过将hasNulls转换成泛型方法,可以避免使用通配符类型
public static <T> boolean hasNulls(Pair<T> p) { /* ... */ }
// 但是,带有通配符的版本可读性更好。
8.8.4通配符捕获
下面编写一个方法来交换对组的元素:
public static void swap(Pair<?> p) { /* ... */ }
通配符不是类型变量,因此,不能在编写代码中使用
?
作为一种类型。这是一个问题,因为在交换的时候必须临时保存第一个元素。幸运的是,这个问题有一个有趣的解决方案。可以写一个辅助方法swapHelper
:
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
注意,
swapHelper
是一个泛型方法,而swap
不是,它有一个固定的Pair<?>
类型的参数。现在可以由swap
调用swapHelper
:
public static void swap(Pair<?> p) {
swapHelper(p);
}
在这种情况下,
swapHelper
方法的参数T
捕获通配符。它不知道通配符指示哪种类型,但是,这是一个明确的类型,并且从<T> swapHelper
的定义可以清楚地看到T
指示那个类型。
当然,在这种情况下,并不是一定要使用通配符。也可以直接把<T> void swapHelper(Pair<T> p)
实现为一个没有通配符的泛型方法。不过,考虑下面这个例子,这里通配符类型很自然地出现在计算中间:
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
}
在这里,通配符捕获机制是不可避免的。
通配符捕获只有在非常限定的情况下才是合法的。编译器必须能够保证通配符表示单个确定的类型。例如,ArrayList<Pair<T>>
中的T
永远不能捕获ArrayList<Pair<?>>
中的通配符。数组列表可以保存两个Pair<?>
,其中的?
分别有不同的类型。
public class PairTest3 {
public static void main(String[] args) {
var ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);
var cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);
var buddies = new Pair<Manager>(ceo, cfo);
printBuddies(buddies);
ceo.setBonus(1000000);
cfo.setBonus(500000);
Manager[] managers = { ceo, cfo };
var result = new Pair<Employee>();
minmaxBonus(managers, result);
System.out.println("first: " + result.getFirst().getName() + ", second: " + result.getSecond().getName());
maxminBonus(managers, result);
System.out.println("first: " + result.getFirst().getName() + ", second: " + result.getSecond().getName());
}
public static void printBuddies(Pair<? extends Employee> p) {
Employee first = p.getFirst();
Employee second = p.getSecond();
System.out.println(first.getName() + " and " + second.getName() + " are buddies.");
}
public static void minmaxBonus(Manager[] a, Pair<? super Manager> result) {
if (a.length == 0) {
return;
}
Manager min = a[0];
Manager max = a[0];
for (int i = 1; i < a.length; i++) {
if (min.getBonus() > a[i].getBonus()) {
min = a[i];
}
if (max.getBonus() < a[i].getBonus()) {
max = a[i];
}
}
result.setFirst(min);
result.setSecond(max);
}
public static void maxminBonus(Manager[] a, Pair<? super Manager> result) {
minmaxBonus(a, result);
PairAlg.swapHelper(result); // OK--swapHelper captures wildcard type
}
// Can't write public static <T super manager> ...
}
class PairAlg {
public static boolean hasNulls(Pair<?> p) {
return p.getFirst() == null || p.getSecond() == null;
}
public static void swap(Pair<?> p) {
swapHelper(p);
}
public static <T> void swapHelper(Pair<T> p) {
T t = p.getFirst();
p.setFirst(p.getSecond());
p.setSecond(t);
}
}
8.9反射和泛型
8.9.1泛型Class
类
现在,
Class
类是泛型的。例如,String.class
实际上是一个Class<String>
类的对象(事实上,是唯一的对象)。
类型参数十分有用,这是因为它允许Class<T>
方法的返回类型更加具有特定性。其他的相关方法见API。
8.9.2使用Class<T>
参数进行类型匹配
匹配泛型方法中
Class<T>
参数的类型变量有时会很有用。例如:
public static <T> Pair<T> makePair(Class<T> c) throws InstantiationException, IllegalAccessException {
return new Pair<>(c.newInstance(), c.newInstance());
}
如果调用
makePair(Employee.class)
,Employee.class
将是一个Class<Employee>
类型的对象。makePair
方法的类型参数T
同Employee
匹配,编译器可以推断出这个方法将返回一个Pair<Employee>
。
8.9.3虚拟机中的泛型类型信息
Java泛型的突出特性之一是在虚拟机中擦除泛型类型。令人奇怪的是,擦除的类仍然保留原先泛型的微弱记忆。例如,原始的
Pair
类知道它源于泛型类Pair<T>
,尽管一个Pair
类型的对象无法区分它是构造为Pair<String>
还是Pair<Employee>
。
类似地,考虑以下方法:
public static Comparable min(Comparable[] a);
// 这是擦除以下泛型方法得到的:
public static <T extends Comparable<? super T>> T min(T[] a);
可以使用反射API来确定:
- 这个泛型方法有一个名为
T
的类型参数。- 这个类型参数有一个子类型限定,其自身又是一个泛型类型。
- 这个限定类型有一个通配符参数。
- 这个通配符参数有一个超类型限定。
- 这个泛型方法有一个泛型数组参数。
换句话说,可以重新构造实现者声明的泛型类和方法的所有有关内容。但是,不会知道对于特定的对象或方法调用会如何解析类型参数。
为了表述泛型类型声明,可以使用java.lang.reflect
包中的接口Type
。其他的相关方法见API。
8.9.4类型字面量
有时,会希望由值的类型决定程序的行为。例如,在一种持久存储机制中,可能希望用户指定一种方法来保存某个特定类的对象。通常的实现方法是将
Class
对象与一个动作关联。
不过,如果有泛型类,擦除会带来问题。比如说,ArrayList<Integer>
和ArrayList<String>
都擦除为同一个原始类型ArrayList
,如何让它们有不同的动作。
这里有一个技巧,在某些情况下可以解决这个问题。可以捕获Type
接口的一个实例。然后构造一个匿名子类:
var type = new TypeLiteral<ArrayList<Integer>>(){}; // Note the {}
// TypeLiteral构造器会捕获泛型超类型:
class TypeLiteral<T> {
private Type type;
/**
* This constructor must be invoked from an anonymous subclass as new TypeLiteral<...>(){}
*/
public TypeLiteral() {
Type parentType = getClass().getGenericSuperclass();
if (parentType instanceof ParameterizedType) {
type = ((ParameterizedType) parentType).getActualTypeArguments()[0];
} else {
throw new UnsupportedOperationException("Construct as new TypeLiteral<...>(){}");
}
// ...
}
}
如果运行时有一个泛型类型,就可以将它与
TypeLiteral
匹配。无法从一个对象得到泛型类型(已经被擦除)。不过,字段和方法参数的泛型类型还留存在虚拟机中。
CDI和Guice等注入框架就使用类型字面量来控制泛型类型的注入。