《Java核心技术卷一》第八章
第8章 泛型程序设计
8.1 为什么要使用泛型程序设计
泛型程序设计(generic programming) 意味着编写的代码可以对多种不同类型的对象重用。例如,你并不希望为收集 String 和 File 对象分别编写不同的类。实际上一个 ArrayList 类就可以收集任何类的对象。
8.1.1 类型参数的好处
在 Java 中增加泛型类之前,泛型程序设计是用继承(inheritance)实现的。ArrayList 类只维护一个 Object 引用的数组:
1 | |
这种方法存在两个问题。获取一个值时必须进行强制类型转换:
1 | |
此外,这里没有错误检查。可以向数组列表中添加任何类的值:
1 | |
对于这个调用,编译和运行都不会出错。不过在其他地方,如果将 get 的结果强制类型转换为 String 类型,就会产生一个错误。
泛型提供了一个更好的解决方案:类型参数(type parameter)。ArrayList 类现在有一个类型参数用来指示元素的类型:
1 | |
这使得代码具有更好的可读性。人们一看就知道这个数组列表中包含的是 String 对象。
注释:如果用一个明确的类型而不是
var声明一个变量,则可以通过使用“菱形”语法省略构造器中的类型参数:
1ArrayList<String> files = new ArrayList<>();省略的类型可以从变量的类型推断得出。
Java 9 扩展了菱形语法的使用范围,原先不接受这种语法的地方现在也可以使用了。例如,现在可以对匿名子类使用菱形语法:
1
2
3
4
5
6
7ArrayList<String> passwords = new ArrayList<>()
// diamond OK in Java9
{
public String get(int n) {
return super.get(n).replaceAll(".", "*");
}
};
编译器也可以充分利用这个类型信息。调用 get 的时候,不再需要强制类型转换。编译器知道返回值类型为 String,而不是 Object:
1 | |
编译器还知道 ArrayList<String> 的 add 方法有一个类型为 String 的参数,这比有一个 Object 类型的参数要安全得多。现在,编译器会检查,防止你插入错误类型的对象。例如,以下语句:
1 | |
是无法通过编译的。不过,得到编译错误要比运行时出现类的强制类型转换异常好得多。
这正是类型参数的魅力所在:它们会让你的程序更易读,也更安全。
8.2 定义简单泛型类
**泛型类(generic class)就是有一个或多个类型变量的类。**本章使用一个简单的 Pair 类作为例子。这个类使我们可以只关注泛型,而不用为数据存储的细节而分心。下面是泛型 Pair 类的代码:
1 | |
Pair 类引入了一个类型变量 T,用尖括号(<>)括起来,放在类名的后面。**泛型类可以有多个类型变量。**例如,可以定义 Pair 类,其中第一个字段和第二个字段使用不同的类型:
1 | |
**类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。**例如:
1 | |
注释:常见的做法是类型变量使用大写字母,而且很简短。Java 类库使用变量
E表示集合的元素类型,K和V分别表示表的键和值的类型。T(必要时还可以用相邻的字母U和S)表示“任意类型”。
可以用具体的类型替换类型变量来**实例化(instantiate)**泛型类型,例如:
1 | |
可以把结果想象成一个普通类,它有以下构造器:
1 | |
以及以下方法:
1 | |
换句话说,泛型类相当于普通类的工厂。
8.3 泛型方法
可以定义一个带有类型参数的方法。
1 | |
这个方法是在普通类中定义的,而不是在泛型类中。不过,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里的修饰符就是 public static)的后面,并在返回类型的前面。
可以在普通类中定义泛型方法,也可以在泛型类中定义。
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
1 | |
在这种情况下,方法调用中可以省略 <String> 类型参数。编译器有足够的信息推断出你想要的方法。它将参数的类型与泛型类型 T... 进行匹配,推断出 T 一定是 String。也就是说,可以简单地调用:
1 | |
几乎在所有情况下,泛型方法的类型推导都能正常工作。偶尔,编译器也会提示错误,此时你就需要解译错误报告。考虑下面这个示例:
1 | |
错误消息以晦涩的方式指出:解释这个代码有两种方式,而且这两种方式都是合法的。简单地说,编译器将把参数自动装箱为 1 个 Double 和 2 个 Integer 对象,然后寻找这些类的共同超类型。事实上,它找到了 2 个超类型:Number 和 Comparable 接口,Comparable 接口本身也是一个泛型类型。在这种情况下,可以采取的补救措施是将所有的参数都写为 double 值。
提示:如果想知道编译器对一个泛型方法调用最终推断出哪种类型:故意引入一个错误,然后研究所得到的错误消息。例如,考虑调用
ArrayAlg.getMiddle("Hello", 0, null)。将结果赋给JButton,这肯定是不对的。将会得到一个错误报告:
1
2
3found:
java.lang.Object & java.io.Serializable & java.lang.Comparable<? extends
java.lang.Object & java.io.Serializable & java.lang.Comparable<?>>大致的意思是:可以将结果赋给
Object、Serializable或Comparable。
8.4 类型变量的限定
有时,类或方法需要对类型变量加以约束。下面是一个典型的例子。我们要计算数组中的最小元素:
1 | |
但是,这里有一个问题。请看 min 方法的代码。变量 smallest 的类型为 T,这意味着它可以是任何一个类的对象。如何知道 T 所属的类有一个 compareTo 方法呢?
解决这个问题的办法是限制 T 只能是实现了 Comparable 接口(包含一个方法 compareTo 的标准接口)的一个类。可以通过对类型变量 T 设置一个限定(bound) 来实现这一点:
1 | |
实际上 Comparable 接口本身就是一个泛型类型。
现在,泛型方法 min 只能在实现了 Comparable 接口的类(如 String、LocalDate 等)的数组上调用。因为 Rectangle 类没有实现 Comparable 接口,所以在 Rectangle 数组上调用 min 将会得到一个编译错误。
你或许会感到奇怪——在这里我们为什么使用关键字 extends 而不是 implements?毕竟,Comparable 是一个接口。下面的记法:
1 | |
表示 T 应该是限定类型(bounding type)的子类型(subtype)。T 和限定类型可以是类,也可以是接口。选择关键字 extends 的原因是它更接近子类型的概念,并且 Java 的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
一个类型变量或通配符可以有多个限定,例如:
1 | |
限定类型用 & 分隔,而逗号用来分隔类型变量。
按照 Java 继承机制,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。
8.5 泛型代码和虚拟机
**虚拟机没有泛型类型对象——所有对象都属于普通类。**在泛型实现的早期版本中,甚至能够将使用泛型的程序编译为在 1.0 虚拟机上运行的类文件!
8.5.1 类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为 Object)。
例如,Pair<T> 的原始类型如下所示:
1 | |
因为 T 是一个无限定的类型变量,所以直接替换为 Object。
其结果是一个普通类,就好像 Java 语言中引入泛型之前实现的类一样。
在程序中可以包含不同类型的 Pair,例如,Pair<String> 或 Pair<LocalDate>。不过擦除类型后,它们都会变成原始的 Pair 类型。
原始类型用第一个限定来替换类型变量,或者,如果没有给定限定,就替换为 Object。
例如,类 Pair<T> 中的类型变量没有显式的限定,因此,原始类型用 Object 替换 T。假定我们声明了一个稍有不同的类型:
1 | |
原始类型 Interval 如下所示:
1 | |
注释:你可能想要知道限定切换为
class Interval<T extends Serializable & Comparable>会发生什么。如果这样做,原始类型会用Serializable替换T,而且编译器会在必要时插入转换为Comparable的强制类型转换。为了提高效率,应该将标记(tagging)接口(即没有方法的接口,例如Serializable)放在限定列表的末尾。
8.5.2 转换泛型表达式
**编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。**例如,对于下面这个语句序列:
1 | |
getFirst 擦除类型后的返回类型是 Object。编译器自动插入转换到 Employee 的强制类型转换。也就是说,编译器把这个方法调用转换为两条虚拟机指令:
- 调用原始方法
Pair.getFirst。 - 将返回的
Object类型强制转换为Employee类型。
**访问一个泛型字段时也会插入强制类型转换。**假设 Pair 类的 first 字段和 second 字段都是公共的(也许这不是一种好的编程风格,但在 Java 中是合法的)。以下表达式:
1 | |
也会在结果字节码中插入强制类型转换。
8.5.3 转换泛型方法
**类型擦除也会出现在泛型方法中。**程序员通常认为类似下面的泛型方法:
1 | |
是整个一组方法,而擦除类型之后,只剩下一个方法:
1 | |
注意,类型参数 T 已经被擦除了,只留下了它的限定类型 Comparable。
方法的擦除带来了两个复杂问题。考虑下面这个示例:
1 | |
日期区间是一对 LocalDate 对象,而且我们想覆盖这个方法来确保第二个值永远不小于第一个值。这个类擦除后变成:
1 | |
令人感到奇怪的是,还有另一个从 Pair 继承的 setSecond 方法,即:
1 | |
这显然是一个不同的方法,因为它有一个不同类型的参数——Object,而不是 LocalDate。不过,它不应该不一样。考虑下面的语句序列:
1 | |
这里,我们希望 setSecond 调用具有多态性,应该调用适当的方法。因为 pair 引用一个 DateInterval 对象,所以应该调用 DateInterval.setSecond。问题在于类型擦除与多态发生了冲突。为了解决这个问题,编译器在 DateInterval 类中生成一个桥方法(bridge method):
1 | |
要想了解为什么这样可行,请仔细跟踪以下语句的执行:
1 | |
变量 pair 已经声明为类型 Pair<LocalDate>,并且这个类型只有一个名为 setSecond 的方法,即 setSecond(Object)。虚拟机在 pair 引用的对象上调用这个方法。这个对象是 DateInterval 类型,因而将会调用 DateInterval.setSecond(Object) 方法。这个方法是合成的桥方法。它会调用 DateInterval.setSecond(LocalDate),这正是我们想要的。
桥方法可能会变得更奇怪。假设 DateInterval 类也覆盖了 getSecond 方法:
1 | |
在 DateInterval 类中,有两个 getSecond 方法:
1 | |
你不能编写这样的 Java 代码(两个方法有相同的参数类型是不合法的,在这里,两个方法都没有参数)。但是,在虚拟机中,会由参数类型以及返回类型共同指定一个方法。因此,编译器可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况。
注释:桥方法不只是用于泛型类型。一个方法覆盖另一个方法时,可以指定一个更严格的返回类型,这是合法的。例如:
1
2
3public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException { ... }
}
Object.clone和Employee.clone方法被称为有协变的返回类型(covariant return type)。实际上,Employee类有两个克隆方法:
1
2Employee clone() // defined above
Object clone() // synthesized bridge method, overrides Object.clone合成的桥方法会调用新定义的方法。
总之,对于 Java 泛型的转换,需要记住以下几点:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。
- 会合成桥方法来保持多态。
- 为保持类型安全性,必要时会插入强制类型转换。
8.5.4 调用遗留代码
设计 Java 泛型时,主要目标是允许泛型代码和遗留代码之间能够互操作。下面看有关遗留代码的一个具体示例。Swing 用户界面工具包提供了一个 JSlider 类,它的“刻度”(tick)可以定制为包含文本或图像的标签。这些标签用以下调用设置:
1 | |
Dictionary 类将整数映射到标签。在 Java 5 之前,这个类实现为一个 Object 实例映射。Java 5 把 Dictionary 实现为一个泛型类,不过 JSlider 从未更新。此时,没有类型参数的 Dictionary 是一个原始类型。这里就存在兼容性问题。
填充字典时,可以使用泛型类型:
1 | |
将 Dictionary<Integer, Component> 对象传递给 setLabelTable 时,编译器会发出一个警告:
1 | |
毕竟,编译器无法确定 setLabelTable 可能会对 Dictionary 对象做什么操作。这个方法可能会把字典的所有键替换为字符串。这就打破了键类型必须为 Integer 的承诺,未来的操作有可能导致糟糕的强制类型转换异常。
要仔细考虑这个问题,想想看 JSlider 到底会用 Dictionary 对象做什么。在这里十分清楚,JSlider 只读取这个信息,因此可以忽略这个警告。
现在看一个相反的情形,由一个遗留类得到一个原始类型的对象。可以将它赋给一个类型使用了泛型的变量,当然,这样做会得到一个警告。例如:
1 | |
没关系。查看这个警告,确保标签表确实包含 Integer 和 Component 对象。当然,从来没有绝对的保证。恶意的程序员可能会在滑动条中安装一个不同的 Dictionary。不过,这种情况并不会比有泛型之前的情况更糟糕。最差的情况也就是程序抛出一个异常。
考虑了这个警告之后,可以使用**注解(annotation)**使之消失。可以对一个局部变量加注解,如下所示:
1 | |
或者,可以对整个方法加注解,如下所示:
1 | |
这个注解会关闭对方法中所有代码的检查。
8.6 限制与局限性
8.6.1 不能用基本类型实例化类型参数
**不能用基本类型代替类型参数。**因此,没有 Pair<double>,只有 Pair<Double>。当然,其原因就在于类型擦除。擦除之后,Pair 类含有 Object 类型的字段,而 Object 不能存储 double 值。
这样做与 Java 语言中基本类型的独立状态相一致。这并不是一个致命的缺陷——只有 8 种基本类型,而且即使不能接受包装器类型(wrapper type),也可以使用单独的类和方法来处理。
8.6.2 运行时类型查询只适用于原始类型
虚拟机中的对象总是有一个特定的非泛型类型。因此,所有的类型查询只生成原始类型。例如:
1 | |
实际上仅仅测试 a 是否是任意类型的一个 Pair。下面的测试同样如此:
1 | |
或以下强制类型转换也是如此:
1 | |
为了提醒这一风险,如果试图查询一个对象是否属于某个泛型类型,你会得到一个编译器错误(使用 instanceof 时),或者得到一个警告(使用强制类型转换时)。
同样的道理,getClass 方法总是返回原始类型。例如:
1 | |
这个比较的结果是 true,因为两个 getClass 调用都返回 Pair.class。
8.6.3 不能创建参数化类型的数组
**不能实例化参数化类型的数组,**例如:
1 | |
这有什么问题呢?擦除之后,table 的类型是 Pair[]。可以把它转换为 Object[]:
1 | |
数组会记住它的元素类型,如果试图存储类型不正确的元素,就会抛出一个 ArrayStoreException 异常:
1 | |
不过对于泛型类型,擦除会使这种机制无效。以下赋值
1 | |
尽管能够通过数组存储的检查,但仍会导致一个类型错误。出于这个原因,不允许创建参数化类型的数组。
需要说明的是,只是不允许创建这些数组,而声明类型为 Pair<String>[] 的变量仍是合法的。不过不能用 new Pair<String>[10] 初始化这个变量。
注释:可以声明通配类型的数组,然后进行强制类型转换:
var table = (Pair
[]) new Pair<?>[10]; 结果将是不安全的。如果在
table[0]中存储一个Pair<Employee>,然后对table[0].getFirst()调用一个String方法,会得到一个ClassCastException异常。提示:如果需要收集参数化类型对象,可以直接使用
ArrayList:ArrayList<Pair<String>>很安全也很有效。
8.6.4 Varargs 警告
Java 不支持泛型类型的数组。向参数个数可变的方法传递一个泛型类型的实例。
考虑下面这个简单的方法,它的参数个数是可变的:
1 | |
回忆一下,实际上参数 ts 是一个数组,包含提供的所有实参。
现在考虑以下调用:
1 | |
为了调用这个方法,Java 虚拟机必须建立一个 Pair<String> 数组,这就违反了规则。不过,对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解:
1 | |
或者在 Java 7 中,还可以用 @SafeVarargs 直接注解 addAll 方法:
1 | |
现在就可以提供泛型类型来调用这个方法了。对于任何只需要读取参数数组元素的方法(这肯定是最常见的情况),都可以使用这个注解。
@SafeVarargs 只能用于声明为 static、final 或(Java 9 中)private 的构造器和方法。所有其他方法都可能被覆盖,这会使这个注解失去意义。
注释:可以使用
@SafeVarargs注解来消除创建泛型数组的有关限制,方法如下:
1
2@SafeVarargs
static <E> E[] array(E... array) { return array; }现在可以调用:
1Pair<String>[] table = array(pair1, pair2);这看起来很方便,不过隐藏着危险。以下代码
1
2Object[] objarray = table;
objarray[0] = new Pair<Employee>();能顺利运行而不会出现
ArrayStoreException异常(因为数组存储只会检查擦除后的类型),但在处理table[0]时,你会在别处得到一个异常。
8.6.5 不能实例化类型变量
不能在类似 new T(...) 的表达式中使用类型变量。例如,下面的 Pair<T> 构造器就是非法的:
1 | |
类型擦除将 T 变成 Object,而你肯定不希望调用 new Object()。
在 Java 8 之后,最好的解决办法是让调用者提供一个构造器表达式。例如:
1 | |
makePair 方法接收一个 Supplier<T>,这是一个函数式接口,表示一个无参数而且返回类型为 T 的函数:
1 | |
比较传统的解决方法是通过反射调用 Constructor.newInstance 方法来构造泛型对象。
遗憾的是,细节有点复杂。不能如下调用:
1 | |
表达式 T.class 是不合法的,因为它会擦除为 Object.class。必须适当地设计 API 以便得到一个 Class 对象,如下所示:
1 | |
这个方法可以如下调用:
1 | |
注意,Class 类本身是泛型的。例如,String.class 是 Class<String> 的一个实例(事实上,它是唯一的实例)。因此,makePair 方法能够推断出所建立的对组(pair)的类型。
8.6.6 不能构造泛型数组
不能实例化数组。数组本身也带有类型,用来监控虚拟机中的数组存储。这个类型会被擦除。例如,考虑下面的例子:
1 | |
类型擦除会让这个方法总是构造 Comparable[2] 数组。
如果数组仅仅作为一个类的私有实例字段,那么可以将这个数组的元素类型声明为擦除后的类型并使用强制类型转换。例如,ArrayList 类可以如下实现:
1 | |
但实际的实现没有这么清晰:
1 | |
这里,强制类型转换 (E[]) 是一个假象,而类型擦除使其无法察觉。
这个技术并不适用于我们的 minmax 方法,因为 minmax 方法返回一个 T[] 数组,如果我们对类型“作假”,使用擦除后的类型,就会得到运行时错误结果。假设实现以下代码:
1 | |
以下调用
1 | |
编译时不会有任何警告。当方法返回后 Comparable[] 引用被强制转换为 String[] 时,将会出现 ClassCastException 异常。
在这种情况下,最好让用户提供一个数组构造器表达式:
1 | |
构造器表达式 String[]::new 指示一个函数,给定所需的长度,会构造一个指定长度的 String 数组。
minmax 方法使用这个参数生成一个有正确类型的数组:
1 | |
比较老式的方法是利用反射,并调用 Array.newInstance:
1 | |
ArrayList 类的 toArray 方法就没有这么幸运。它需要生成一个 T[] 数组,但没有元素类型。因此,有下面两种不同的形式:
1 | |
第二个方法接收一个数组参数。如果数组足够大,就使用这个数组。否则,用 result 的元素类型构造一个足够大的新数组。
8.6.7 泛型类的静态上下文中类型变量无效
不能在静态字段或方法中引用类型变量。例如,下面的做法看起来很聪明,但实际上行不通:
1 | |
如果这样可行,程序就可以声明一个 Singleton<Random> 以共享一个随机数生成器,另外声明一个 Singleton<JFileChooser> 以共享一个文件选择器对话框。但是,这样是行不通的。类型擦除之后,只剩下 Singleton 类,它只包含一个 singleInstance 字段。因此,带有类型变量的静态字段和方法是完全非法的。
8.6.8 不能抛出或捕获泛型类的实例
**既不能抛出也不能捕获泛型类的对象。**实际上,甚至泛型类扩展 Throwable 都是不合法的。例如,以下定义就不能编译:
1 | |
catch 子句中不能使用类型变量。例如,以下方法将不能编译:
1 | |
不过,在异常规范中使用类型变量是允许的。以下方法是合法的:
1 | |
8.6.9 可以取消对检查型异常的检查
Java 异常处理的一个基本原则是,必须为所有检查型异常提供一个处理器。不过可以利用泛型取消这个机制。关键在于以下方法:
1 | |
假设这个方法包含在接口 Task 中,如果有一个检查型异常 e,并调用
1 | |
编译器就会认为 e 是一个非检查型异常。以下代码会把所有异常都转换为编译器所认为的非检查型异常:
1 | |
下面使用这个技术解决一个棘手的问题。要在一个线程中运行代码,需要把代码放在一个实现了 Runnable 接口的类的 run 方法中。不过这个方法不允许抛出检查型异常。我们将提供一个从 Task 到 Runnable 的适配器,它的 run 方法可以抛出任意的异常。
1 | |
例如,以下程序运行了一个线程,它会抛出一个检查型异常。
1 | |
Thread.sleep 方法声明为抛出一个 InterruptedException,我们不再需要捕获这个异常。因为我们没有中断这个线程,所以不会抛出这个异常。不过,程序会抛出一个检查型异常。运行程序时,你会得到一个栈轨迹。
这有什么意义呢?正常情况下,你必须捕获一个 Runnable 的 run 方法中的所有检查型异常,把它们“包装”到非检查型异常中,因为 run 方法声明为不抛出任何检查型异常。
不过在这里并没有做这种“包装”。我们只是抛出异常,并“哄骗”编译器,让它相信这不是一个检查型异常。
通过使用泛型类、擦除和 @SuppressWarnings 注解,我们就能消除 Java 类型系统的一个基本限制。
8.6.10 注意擦除后的冲突
擦除泛型类型后,不允许创建引发冲突的条件。下面来看一个示例。假定为 Pair 类增加一个 equals 方法,如下所示:
1 | |
考虑一个 Pair<String>。从概念上讲,它有两个 equals 方法:
1 | |
但是,直觉把我们引入歧途。方法
1 | |
擦除后就是
1 | |
这会与 Object.equals 方法发生冲突。
当然,补救的办法是重新命名引发冲突的方法。
泛型规范还指出了另外一个规则:“为了支持擦除转换,我们要施加一个限制:倘若两个接口类型是同一接口的不同参数化,一个类或类型变量就不能同时作为这两个接口类型的子类。”例如,下面的代码是非法的:
1 | |
如果以上代码可行,Manager 就会实现 Comparable<Employee> 和 Comparable<Manager>,而它们是同一接口的不同参数化。
这一限制与类型擦除的关系并不十分明显。毕竟,以下非泛型版本是合法的:
1 | |
其原因非常微妙,这有可能与合成的桥方法产生冲突。实现了 Comparable<X> 的类会获得一个桥方法:
1 | |
不可能对不同的类型 X 有两个这样的方法。
8.7 泛型类型的继承规则
考虑一个类和一个子类,如 Employee 和 Manager。Pair<Manager> 是 Pair<Employee> 的一个子类型吗?或许人们会感到奇怪,答案是“不是”。例如,下面的代码将不能成功编译:
1 | |
一般来讲,无论 S 与 T 有什么关系,Pair<S> 与 Pair<T> 都没有任何关系(如图 8-1 所示)。

这看起来是一个很严格的限制,不过对于类型安全非常必要。假设允许将 Pair<Manager> 转换为 Pair<Employee>。考虑下面的代码:
1 | |
显然,最后一句是合法的。但是 employeeBuddies 和 managerBuddies 引用了同样的对象。现在我们会把 CFO 和一个底层员工组成一对,这对于 Pair<Manager> 来说应该是不可能的。
注释:可以将一个
Manager[]数组赋给一个类型为Employee[]的变量:
1
2Manager[] managerBuddies = {ceo, cfo};
Employee[] employeeBuddies = managerBuddies; // OK不过,数组有特别的保护。如果试图将一个底层员工存储到
employeeBuddies[0],虚拟机将会抛出ArrayStoreException异常。
总是可以将参数化类型转换为一个原始类型。例如,Pair<Employee> 是原始类型 Pair 的一个子类型。在与遗留代码交互时,这个转换非常必要。
转换成原始类型会导致类型错误吗?很遗憾,会!看一看下面这个示例:
1 | |
当使用 getFirst 获得外来对象并赋给 Manager 变量时,与以前一样,会抛出 ClassCastException 异常。这里失去的只是泛型程序设计提供的附加安全性。
最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,它们与普通的类没有什么区别。例如,ArrayList<T> 类实现了 List<T> 接口。这意味着,一个 ArrayList<Manager> 可以转换为一个 List<Manager>。但是,如前面所见,ArrayList<Manager> 不是一个 ArrayList<Employee> 或 List<Employee>。图 8-2 展示了它们之间的这种关系。

8.8 通配符类型
Java 的设计者发明了一种巧妙的(但很安全的)“逃生出口”:通配符类型(wildcard type)。下面几小节会介绍如何使用通配符。
8.8.1 通配符概念
**在通配符类型中,允许类型参数变化。**例如,通配符类型
1 | |
表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类,如 Pair<Manager>,但不能是 Pair<String>。
假设要编写一个打印员工对的方法,如下所示:
1 | |
正如前面讲到的,不能将 Pair<Manager> 传递给这个方法,这一点很有限制。不过解决的方法很简单——可以使用一个通配符类型:
1 | |
类型 Pair<Manager> 是 Pair<? extends Employee> 的子类型(如图 8-3 所示)。

使用通配符会通过 Pair<? extends Employee> 的引用破坏 Pair<Manager> 吗?
1 | |
这不可能引起破坏。对 setFirst 的调用有一个类型错误。要了解其中的缘由,请仔细看一看类型 Pair<? extends Employee>。它的方法如下:
1 | |
不可能调用 setFirst 方法。考虑调用 wildcardBuddies.setFirst(lowlyEmployee),编译器知道 setFirst 的参数有某个特定的类型,这个类型扩展了 Employee。这个特定类型是 Employee 吗?是 Manager 吗?还是另外某个子类?编译器无法知道。因此,编译器不能接受 lowlyEmployee。出于同样的原因,调用 wildcardBuddies.setFirst(cio)(其中 cio 是一个 Manager 实例)也会出错。除了 null,编译器必须拒绝传入 setFirst 的所有参数。
getFirst 方法则可以继续工作。getFirst 的返回值是某个特定类型的实例,这是 Employee 的一个子类型。编译器不知道这个特定类型是什么,但它可以保证对 Employee 引用的赋值是安全的。
这就是引入有限定的通配符的关键之处。现在我们已经有办法区分安全的访问器方法和不安全的更改器方法了。
8.8.2 通配符的超类型限定
通配符限定与类型变量限定十分类似,你可以指定一个超类型限定(supertype bound),如下所示:
1 | |
这个通配符限制为 Manager 的所有超类型。
为什么想要这样做呢?带有超类型限定的通配符会提供一种行为。可以为方法提供参数,但不能使用返回值。例如,Pair<? super Manager> 有一些方法可以描述如下:
1 | |
这不是真正的 Java 语法,但是可以展示编译器知道什么。setFirst 的参数类型表示为 ? super Manager,这是某个特定类型 T,而 Manager 是 T 的一个子类型。对于 T 实际上有 3 种选择:Object、Employee 或 Manager。(如果 Manager 或 Employee 实现了接口,可能还有更多选择)。不过,编译器无法知道其中哪个选择正确。所以,编译器不能接受参数类型为 Employee 或 Object 的调用。毕竟,T 可能是 Manager。只能传递 Manager 类型或某个子类型(如 Executive)的对象。
另外,如果调用 getFirst,不能保证返回对象的类型。只能把它赋给一个 Object。
下面是一个典型的示例。我们有一个经理数组,并且想把奖金最高和最低的经理放在一个 Pair 对象中。Pair 的类型是什么?在这里,Pair<Employee> 是合理的,或者对此而言,
Pair<Object> 也是合理的(如图 8-4 所示)。下面的方法将接受任何合适的 Pair:
1 | |

直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型限定的通配符允许你读取一个泛型对象。
下面是超类型限定的另一种应用。Comparable 接口本身就是一个泛型类型。声明如下:
1 | |
在这里,类型变量指示了 other 参数的类型。例如,String 类实现了 Comparable<String>,它的 compareTo 方法声明为
1 | |
这很好,显式参数有正确的类型。接口是泛型接口之前,other 是一个 Object,这个方法的实现中必须有一个强制类型转换。
由于 Comparable 是一个泛型类型,对于 ArrayAlg 类的 minmax 方法,也许我们还能做得更好一些?可以将它声明为:
1 | |
看起来,这样比只使用 T extends Comparable 更彻底,而且对于很多类都能很好地工作。例如,如果计算一个 String 数组的最小值,T 就是类型 String,而 String 是 Comparable<String> 的一个子类型。但是,处理一个 LocalDate 对象数组时,我们会遇到一个问题。LocalDate 实现了 ChronoLocalDate,而 ChronoLocalDate 扩展了 Comparable<ChronoLocalDate>。因此,LocalDate 实现的是 Comparable<ChronoLocalDate> 而不是 Comparable<LocalDate>。
在这种情况下,可以利用超类型来解决:
1 | |
现在 compareTo 方法形式如下:
1 | |
它可以声明为接受类型 T 的对象,或者也可以是 T 的一个超类型的对象(例如,当 T 是 LocalDate 时)。无论如何,都可以安全地向 compareTo 方法传递一个 T 类型的对象。
对于初学者来说,类似 <T extends Comparable<? super T>> 的声明看起来有点吓人。很遗憾,因为这个声明的本意是帮助开发应用的程序员去除对调用参数的不必要的限制。对泛型没有兴趣的应用程序员可能很快就会略过这些声明,想当然地认为库程序员做的都是正确的。如果你是一名库程序员,一定要熟悉通配符,否则,就会受到用户的责备,他们要在代码中随机地添加强制类型转换直至代码能够编译。
注释:超类型限定的另一个常见的用法是作为一个函数式接口的参数类型。例如,
Collection接口有一个方法:
1default boolean removeIf(Predicate<? super E> filter)这个方法会删除所有满足给定谓词条件的元素。例如,如果你不喜欢有奇怪散列码的员工,就可以如下将他们删除:
1
2
3ArrayList<Employee> staff = ...;
Predicate<Object> oddHashCode = obj -> obj.hashCode() % 2 != 0;
staff.removeIf(oddHashCode);你希望能够传入一个
Predicate<Object>,而不只是Predicate<Employee>。super通配符可以使这个愿望成真。
8.8.3 无限定通配符
甚至还可以使用根本无限定的通配符,例如,Pair<?>。初看起来,这好像与原始的 Pair 类型一样。实际上,这两种类型有很大的不同。类型 Pair<?> 有以下方法:
1 | |
getFirst 的返回值只能赋给一个 Object。setFirst 方法不能调用,甚至不能用 Object 调用。
Pair<?> 和 Pair 本质的不同在于:你可以用任意 Object 对象调用原始 Pair 类的 setFirst 方法。
注释:可以调用
setFirst(null)。
为什么要使用这样一个脆弱的类型?它对于很多简单操作很有用。例如,下面这个方法可用来测试一个对组是否包含一个 null 引用,它不需要具体的类型。
1 | |
通过将 hasNulls 转换成泛型方法,可以避免使用通配符类型:
1 | |
但是,带有通配符的版本可读性更好。
8.8.4 通配符捕获
下面编写一个方法来交换对组的元素:
1 | |
**通配符不是类型变量,**因此,不能编写使用 ? 作为一种类型的代码。也就是说,下面的代码是非法的:
1 | |
这里有一个问题,因为在交换的时候,必须临时保存第一个元素。幸运的是,这个问题有一个有趣的解决方案。我们可以写一个辅助方法 swapHelper,如下所示:
1 | |
注意,swapHelper 是一个泛型方法,而 swap 不是,它有一个固定的 Pair<?> 类型的参数。现在可以由 swap 调用 swapHelper:
1 | |
在这种情况下,swapHelper 方法的参数 T 捕获通配符。并不知道通配符指示哪种类型,但是,这是一个明确的类型,并且从 <T> swapHelper 的定义可以清楚地看到 T 指示那个类型。
当然,在这种情况下,并不是一定要使用通配符。我们也可以直接把 <T> void swap(Pair<T> p) 实现为一个没有通配符的泛型方法。不过,考虑下面这个例子,这里通配符类型很自然地出现在一个计算中间:
1 | |
在这里,通配符捕获机制是不可避免的。
通配符捕获只有在非常有限的情况下是合法的。编译器必须能够保证通配符表示单个确定的类型。例如,ArrayList<Pair<T>> 中的 T 绝对不能捕获 ArrayList<Pair<?>> 中的通配符。数组列表可能包含两个 Pair<?>,其中的 ? 可能分别有不同的类型。
8.9 反射和泛型
反射允许你在运行时分析任意对象。如果对象是泛型类的实例,关于泛型类型参数,你可能得不到多少信息,因为它们已经被擦除了。在下面的小节中,我们将学习利用反射可以获得泛型类的哪些信息。
8.9.1 泛型 Class 类
现在,Class 类是泛型类。例如,String.class 实际上是一个 Class<String> 类的对象(事实上,也是唯一的对象)。
类型参数十分有用,这是因为它允许 Class<T> 的方法有更特定的返回类型。Class<T> 的以下方法就利用了类型参数:
1 | |
newInstance 方法返回这个类的一个实例,由无参数构造器获得。它的返回类型现在声明为 T,其类型与 Class<T> 描述的类相同,这样就免除了强制类型转换。
cast 方法返回给定的对象,如果给定对象的类型实际上是 T 的一个子类型,现在会声明为类型 T,否则,会抛出一个 ClassCastException 异常。
如果这个类不是一个 enum 类或 T 类型枚举值的一个数组,getEnumConstants 方法将返回 null。
最后,getConstructor 与 getDeclaredConstructor 方法返回一个 Constructor<T> 对象。Constructor 类也已经变成泛型,使得它的 newInstance 方法有一个正确的返回类型。
8.9.2 使用 Class<T> 参数进行类型匹配
匹配泛型方法中 Class<T> 参数的类型变量有时会很有用。下面是一个标准的示例:
1 | |
如果调用
1 | |
Employee.class 是一个 Class<Employee> 类型的对象。makePair 方法的类型参数 T 与 Employee 匹配,编译器可以推断出这个方法将返回一个 Pair<Employee>。
8.9.3 虚拟机中的泛型类型信息
Java 泛型的突出特性之一是在虚拟机中擦除泛型类型。令人奇怪的是,擦除的类仍然保留原先泛型的一些微弱记忆。例如,原始 Pair 类知道它源于泛型类 Pair<T>,尽管一个 Pair 类型的对象无法区分它构造为 Pair<String> 还是 Pair<Employee>。
类似地,考虑以下方法:
1 | |
这是擦除以下泛型方法得到的:
1 | |
可以使用反射 API 确定:
- 这个泛型方法有一个名为
T的类型参数。 - 这个类型参数有一个子类型限定,其自身又是一个泛型类型。
- 这个限定类型有一个通配符参数。
- 这个通配符参数有一个超类型限定。
- 这个泛型方法有一个泛型数组参数。
换句话说,你可以重新构造实现者声明的泛型类和方法的所有有关内容。但是,你不会知道对于特定的对象或方法调用会如何解析类型参数。
为了描述泛型类型声明,可以使用 java.lang.reflect 包中的接口 Type。这个接口有以下子类型:
Class类,描述具体类型。TypeVariable接口,描述类型变量(如T extends Comparable<? super T>)。WildcardType接口,描述通配符(如? super T)。ParameterizedType接口,描述泛型类或接口类型(如Comparable<? super T>)。GenericArrayType接口,描述泛型数组(如T[])。
图 8-5 给出了继承层次结构。注意,最后 4 个子类型是接口,虚拟机会实例化实现这些接口的适当的类。

8.9.4 类型字面量
有时,你会希望由值的类型决定程序的行为。例如,在一种持久存储机制中,你可能希望用户指定一种方法来保存某个特定类的对象。通常的实现方法是将 Class 对象与一个动作关联。不过,如果有泛型类,擦除会带来问题。比如说,既然 ArrayList<Integer> 和 ArrayList<String> 都擦除为同一个原始类型 ArrayList,如何让它们有不同的动作呢?
这里有一个技巧,在某些情况下可以解决这个问题。可以捕获 Type 接口(上一节介绍过)的一个实例。然后构造一个匿名子类,如下所示:
1 | |
TypeLiteral 构造器会捕获泛型超类型:
1 | |
如果运行时有一个泛型类型,可以将它与 TypeLiteral 匹配。我们无法从一个对象得到泛型类型(已经被擦除)。不过,正如上一节看到的,字段和方法参数的泛型类型还留存在虚拟机中。
CDI 和 Guice 等注入框架(Injection framework)就使用类型字面量来控制泛型类型的注入。程序清单 8-5 给出了一个更简单的例子。给定一个对象,我们可以罗列它的字段,哪些有泛型类型,并查找相关联的格式化动作。