首先,一些术语。类或者接口,它的声明有一个或者多个类型参数(type parameter),是泛型(generic)类或者接口[JLS, 8.1.2, 9.1.2]。例如,List接口有一个类型参数,E,代表它的元素类型。这个接口的完整名字是List<E>(读作“E的列表”),但是人们常常简单地叫它为List。泛型类和接口全部叫做泛型类型。
每个泛型类型定义了参数化(parameterized)类型的集,它包含了类或者接口名字,紧跟着实际类型参数(actual type parameter)的尖括号列表,这些类型参数是相对于泛型类型的形式类型参数[JLS, 4.4, 4.5]。例如,List<String>(读作“字符串的列表”)是一个参数化类型,代表元素是String类型的一个列表。(String 是相对于形式类型参数E的实际类型参数。)
最后,每个泛型参数定义了一个原生类型(raw type),它是泛型类型的名字,而没有任何相应的参数类型[JLS, 4.8]。例如,相对于List<E>的原生类型是List。原生类型的行为就像所有的泛型类型信息从类型声明中擦除。它们主要是为了和以前的泛型代码相兼容而存在。
在泛型加入到Java之前,以下是一个典型的集合声明。在Java9中,它仍旧是合法的,但是远非可仿效的:
// 原生类型 - 不要这么做!
// 我的邮票集合。仅仅包含Stamp实例。
private final Collection stamps = ... ;
如果你今天使用这个声明,而且意外地把硬币放入到你的邮票集合中,那么这个错误的插入正常编译,而且运行没有错误(尽管编译器发出一个模糊警告):
// 硬币错误插入到邮票集合
stamps.add(new Coin( ... )); // 发出“非受检查的调用”的警告
你不会遇见一个错误,直到你试着从邮票集合中获取这个硬币:
// 原生迭代器类型 - 不要这么用!
for (Iterator i = stamps.iterator(); i.hasNext(); )
Stamp stamp = (Stamp) i.next(); // 抛出ClassCastException
stamp.cancel();
就像这本书自始至终提到的,在构建它们之后,最好在编译的时候,尽早发现错误是值得的。这这个情况中,你直到运行时才会发现错误,远在它发生之后,而且是在远离于包含这个错误的代码的代码中。一旦你看见了ClassCastException,你不得不搜索代码库,寻找把这个硬币放入到邮票集合的方法调用。编译器不会帮你,因为它不能理解注释说“仅仅包含Stamp实例”。
使用泛型,类型声明包含这个信息,而不是注释:
// 参数化的结合类型 - 类型安全的
private final Collection<Stamp> stamps = ... ;
从这个声明中,编译器知道stamps应该仅仅包含Stamp实例,而且保证它是正确的,而且你的整个代码库正常编译,没有发出(或者抑制;参考条目27)任何警告。当stamps以参数化类型声明方式声明时,错误插入产生一个编译时错误信息,精确地告诉你是什么错误:
Test.java:9: error: incompatible types: Coin cannot be converted to Stamp
c.add(new Coin());
^
当你从集合中获取元素时,编译器为你插入了不可见的强转,而且保证了它们不会失败(再次,你的所有代码不会产生或者抑制任何编译警告)。虽然不慎把硬币插入到一个邮票集合的情况可能显得牵强附会的,但是这个问题是实际的。例如,容易想象,把BigInteger放到一个集合,这个集合被认为是只包含BigDecimal实例。
就像前面提到的,使用原生类型(没有类型参数的泛型类型)是合法的,但是你不应该这么做。如果你使用原生类型,那么是失去了泛型的所有优点:安全类型和表达性。既然你不应该使用它们,那么为什么语言设计者起初允许原生类型呢?为了兼容性。当泛型加入时,Java快进入了它的第二个十年,有未使用泛型的大量代码已经存在。这些代码仍旧是合法的,而且和使用泛型的更新代码可以互操作,这注定是至关重要的。它不得不是合法的:把参数化类型的实例传递到一个方法,这个方法是设计为原生类型使用的,或者相反。这个要求,被称为迁移兼容性(migration compatibility),使得做出了这个决定:支持原生类型和使用擦除(erasure)实现泛型(条目28)。
虽然你不应该使用原生类型,比如List,但是使用参数化为允许插入任意对象的类型,比如List<Object>,这是合适的。那么原生类型List和参数化类型List<Object>的区别是什么?大致地讲,前者不在泛型类型系统中,而后者则显式地告诉编译器,它可以留存任何类型的对象。虽然你可以把List<String>传递到类型参数List,但是你不能把它传递到类型参数列表List<Object>。泛型有子类型化的规则,List<String>是原始类型List的子类型,但不是参数化类型List<Object>的子类型(条目28)。因此,如果你使用了原生类型,比如List,那么你失去了类型安全,但是如果你使用参数化类型,比如List<Object>,那么你没有失去。
为了使得这个具体,考虑下面的程序:
// 运行时失败 - unsafeAdd方法使用了原生类型(List)!
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // 编译器产生的强转
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
这个程序可以编译,但是因为它使用了原生类型,你获得一个警告:
Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
list.add(o);
^
事实上,如果你运行这个程序,那么当程序试着把strings.get(0)调用的结果(它是一个Integer)强转为一个String,你得到一个ClassCastException。这是编译器产生的强转,所以他通常保证了成功,但是在这种情况下,我们忽略了一个编译器警告而且付出了代价。
如果你使用unsafeAdd声明中参数化类型List<Object>代替原生类型,而且尝试重新编译这个程序,那么你将发现它不再能编译而且抛出错误信息:
Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42));
^
你可能想着为一个集合使用原生类型,这个集合的元素类型是未知的而且也不重要。例如,假设你想编写一个接受了两个集的方法,而且返回了它们相同元素的个数。如果你对泛型不熟悉,以下是你可能这么编写这样的方法:
// 使用未知元素类型的原生类型 - 不要这么做!
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
}
这个方法起作用,但是它使用了原生类型,这是危险的。安全的替代方案是,使用非受限通配符(unbounded wildcard)类型。如果你想使用一个泛型类型,但是你不知道也不关心实际类型参数是什么,那么你可以用问号替代。例如,泛型类型Set<E>的非受限通配符类型是 Set<?>(读做“某个类型的集”)。这个是最通用的参数化Set类型,它可以保留任何集。以下是使用非受限通配符类型的numElementsInCommon声明看上去的样子:
// 使用无上限通配符类型 - 类型安全的和灵活的
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }
非受限通配符Set<?>和原生类型Set的区别是什么呢?问号真得就适合任何情况?不要过度阐述这个点,但是通配符类型是安全的而原生类型不是。你可以把任何元素放入到一个原生类型的集合,轻易地破坏了集合的类型不变性(就像119页的unsafeAdd方法所展示的);你不要把任何元素(除了null)放入到Collection<?>中。尝试这么做将会产生编译时如下的错误信息:
WildCard.java:13: error: incompatible types: String cannot be converted to CAP#1
c.add("verboten");
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
无可否认,这个错误信息让一些事情有待改进,但是编译器完成了它的工作,防止你破坏集合的类型不变性,不管它的元素类型是什么。不仅你不能把任何元素(除了null)放入到Collection<?>中,而且你不能假设关于你获得对象的类型的任何事情。如果这些限制是不能接受的,那么你应该使用泛型方法(generic method)(条目30),或者受限通配(bounded wildcard)类型(条目31)。
对于你不应该使用原生类型这个规则,有一些小小的例外。你必须在类字面常量中使用原生类型。这个规范不允许使用参数化类型(尽管他不允许array类型和原始类型)[JLS, 15.8.2]。换句话说,List.class、String[].class和int.class全是合法的,但是List<String>.class和List<?>.class不是合法的。
第二个例外是,这个规则是关于instanceof操作子。因为泛型类型信息在运行时擦除,所以在参数化类型上使用instanceof操作子是不合法的,而不是非受限通配符类型。非受限通配符类型,代替原生类型的使用时,在任何情况下都不会影响instanceof操作子的行为。这种情况下,尖括号和问号只是噪音。以下是使用用泛型类型的instanceof操作子的更可取的方法:
// 原生类型的合法使用 - instanceof操作子
if (o instanceof Set) { // 原生类型
Set<?> s = (Set<?>) o; // 通配符类型
...
}
注意到,一旦你决定o是一个Set,那么你必须把它强转到通配符类型Set<?>,而不是原生类型Set。这是一个受检查的强转,所以它不会造成一个编译器警告。
总之,使用原生类型可能导致运行时异常,所以不要使用它们。它们仅仅是为了兼容性和遗留代码的互操作而存在,这个遗留代码早于泛型的引入。快速回顾下,Set<Object>是参数化类型,代表一个可以包含任何类型的对象;Set<?>是通配符类型,代表仅仅包含某个未知类型的对象;Set是一个原生类型,它从泛型类型系统退出了。前两个是安全的,最后一个则不是。
作为一个快速参考,这个条目中(一些由这章后续引入)引入的术语总结在如下表格中:
术语 | 例子 | 条目 |
---|---|---|
参数化类型 | List<String> | 条目 26 |
实际类型参数 | String | 条目 26 |
泛型类型 | List<E> | 条目 26, 29 |
形式类型参数 | E | 条目 26 |
非受限通配符类型 | List<?> | 条目 26 |
原生类型 | List | 条目 26 |
受限类型参数 | <E extends Number> | 条目 29 |
循环类型受限 | <T extends Comparable<T>> | 条目 30 |
受限通配符类型 | List<? extends Number> | 条目 31 |
泛型方法 | static <E> List<E> asList(E[] a) | Item 30 |
类型标记 | String.class | 条目 33 |