第23条:请不要在新代码中使用原生态类型
先来介绍一些术语。声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口[JLS,8.1.2,9.1.2]。例如,从Java 1.5发行版本起,List
接口就只有单个类型参数E
,表示列表的元素类型。从技术的角度来看,这个接口的名称应该是指现在的List<E>
(读作“E
的列表”),但是人们经常把它简称为List
。泛型类和接口统称为泛型(generic type)。
每种泛型定义一组参数化的类型(parameterized type),构成格式为:显示类或者接口的名称,接着用尖括号(< >
)把对应于泛型形式类型参数的实际类型参数列表[JLS,4.4,4.5]括起来。例如,List<String>
(读作“字符串列表”)是一个参数化的类型,表示元素类型为String
的列表。(String
是与形式类型参数E
相对应的实际类型参数。)
最后一点,每个泛型都定义了一个原生态类型(raw type),即不带任何实际类型参数的泛型名称[JLS,4.8]。例如,与List<E>
相对应的原生态类型是List
。原生态类型就像从类型声明中删除了所有泛型信息一样。实际上,原生态类型List
与Java平台没有泛型之前的接口类型List
完全一样。
在Java 1.5版本发行之前,以下集合声明是值得参考的:
// Now a raw collection type - don't do this
/**
* My stamp collection. Contains only Stamp instances.
*/
private final Collection stamps = ...;
如果一不小心将一个 coin 放进了 stamp 集合中,这一错误的插入照样得以编译和运行并且不会出现任何错误提示:
// Erroeous insertion of coin into stamp collection
stamps.add(new Coin(...));
直到从 stamp 集合中获取 coin 时才会收到错误提示:
// Now a raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hashNext(); ) {
Stamp s = (Stamp) i.next(); // Throws ClassCastException
... // Do something with the stamp
}
就如本书中经常提到的,出错之后应该尽快发现,最好是编译时就发现。本例中,直到代码运行时才发现错误,已经出错很久了,而且你在代码中所处的位置距离包含错误的这部分代码已经很远了。一旦发现ClassCastException
,就必须搜索代码,查找将 coin 放进 stamp 集合的方法调用。此时编译器帮不上忙,因为它无法理解这种注释:“Contains only Stamp instances(只包含Stamp
实例)”。
有了泛型,就可以利用改进后的类型声明来代替集合中的这种注释,告诉编译器之前的注释中所隐含的信息:
// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ... ;
通过这条声明,编译器知道stamps
应该只包含Stamp
实例,并给予保证,假设整个代码是利用Java 1.5及其之后版本的编译器进行编译的,所有代码在编译过程中都没有发出(或者禁止,请见第24条)任何警告。当stamps
利用一个参数化的类型进行声明时,错误的插入会产生一条编译时的错误消息,准确地告诉你哪里出错了:
Test.java:9: add(Stamp) in Collection<Stamp> cannot be applied
to (Coin)
stamps.add(new Coin())
^
还有一个好处是,从集合中删除元素时不再需要进行手工转换了。编译器会替你插入隐式的转换,并确保它们不会失败(依然假设所有代码都是通过支持泛型的编译器进行编译的,并且没有产生或者禁止任何警告)。无论你是否使用 for-each 循环(见第46条),上述功能都适用:
// for-each loop over a parameterized collection - typesafe
for (Stamp s : stamps) { // No cast
... // Do something with the stamp
}
或者无论是否使用传统的for
循环也一样:
// for loop with parameterized iterator declaration - typesafe
for (Iterator<Stamp> i = stamps.iterator(); i.hasNext(); ) {
Stamp s = i.next(); // No cast necesary
... // Do something with the stamp
}
虽然假设不小心将 coin 插入到 stamp 集合中可能显得有点牵强,但这类问题确实真实的。例如,很容易想象有人会不小心将一个java.util.Date
实例放进一个原本只包含java.sql.Date
实例的集合中。
如上所述,如果不提供类型参数,使用集合类型和其他泛型也仍然是合法的,但是不应该这么做。如果使用原生态类型,就失掉了泛型在安全性和表述性方面的所有优势。既然不应该使用原生态类型,为什么Java的设计者还要允许使用它们呢?这是为了提供兼容性。因为泛型出现的时候,Java平台即将进入它的第二个10年,已经存在大量没有使用方形的Java代码。人们认为让所有的代码保持合法,并且能够与使用泛型的新代码互用,这一点很重要。它必须合法,才能将参数化类型的实例传递给那些被设计成使用普通类型的方法,反之亦然。这种需求被称作移植兼容性(Migration Compatibility),促成了支持原生态类型的决定。
虽然不应该在新代码中使用像List
这样的原生态类型,使用参数化的类型以允许插入任意对象,如List<Object>
,这还是可以的。原生态类型List
和和参数化的类型List<Object>
之间到底有什么区别呢?不严格地说,前者逃避了泛型检查,后者则明确告知编译器,它能够持有任意类型的对象。虽然你可以将List<String>
传递给List
的参数,但是不能讲它传给类型List<Object>
的参数。泛型有子类型化(subtyping)的规则,List<String>
是原生态类型List
的一个子类型,而不是参数化类型List<Object>
的子类型(见第25条)。因此,如果使用像List
这样的原生态类型,就会失掉类型安全性,但是如果使用像List<Object>
这样的参数化类型,则不会。
为了更具体地进行说明,请参考下面的程序:
// Uses raw type (List) - fails at runtime!
public static void main(String[] args) {
List<String> strings = new ArrayList<String>();
unsafeAdd(strings, new Integer(42));
String s = strings.get(0); // Compiler-generated cast
}
private static void unsafeAdd(List list, Object o) {
list.add(o);
}
这个程序可以进行编译,但是因为它使用了原生态类型List
,你会收到一条警告:
Test.java:10: warning: unchecked call to add(E) in raw type list
list.add(o);
^
实际上,如果运行这段程序,在程序试图将strings.get(0)
的调用结果转换成一个String
时,会收到一个ClassCastException
异常。这是一个编译器生成的转换,因此一般保证会成功,但是我们在这个例子中忽略了一条编译器警告,就会为此而付出代价。
如果在unsafeAdd
声明中使用参数化类型List<Object>
代替原生态类型List
,并试着重新编译这段程序,会发现它无法再进行编译了。以下是它的错误消息:
Test.java:5: unsafeAdd(List<Object>,Object) cannot be applied
to (List<String>,Integer)
unsafeAdd(strings, new Integer(42));
^
在不确定或者不在乎集合中的元素类型的情况下,你也许会使用原生态类型。例如,假设想要编写一个方法,它有两个集合(Set
),并从中返回它们共有的元素的数量。如果你对泛型还不熟悉的话,可以参考一下方式来编写这种方法:
// Use of raw type for unknown element type - don't do this!
static int numElementsInCommon(Set s1, Set s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
}
这个方法倒是可以,但它使用了原生态类型,这是很危险的。从Java 1.5发行版本开始,Java就提供了一种安全的替代方法,称作无限制的通配符类型(unbounded wildcard type)。如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如,泛型Set<E>
的无限制通配符类型为Set<?>
(读作“某个类型的集合”)。这是最普通的参数化Set
类型,可以持有任意集合。下面是numElementsInCommon
方法使用了无限制通配符类型时的情形:
// Unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) {
int result = 0;
for (Object o1 : s1)
if (s2.contains(o1))
result++;
return result;
}
在无限制通配符类型Set<?>
和原生态类型Set
之间有什么区别呢?这个问号真正起到作用了吗?这一点不需要赘述,但通配符类型是安全的,原生态类型则不安全。由于可以将任何元素放进使用原生态类型的集合中,因此很容易破坏该集合的类型约束条件(如第100页的例子中所示的unsafeAdd
方法);但不能将任何元素(除了null
之外)放到`Collection<?>中。如果尝试这么做的话,将会产生一条像这样的编译时错误消息:
WildCard.java:13: cannot find symbol
symbol : method add(String)
location: interface Collection<capture#825 of ?>
c.add("verboten");
^
这样的错误消息显然还无法令人满意,但是编译器已经尽到了它的职责,防止你破坏集合的类型约束条件。你不仅无法将任何元素(除了null
之外)放进Collection<?>
中,而且根本无法猜测你会得到哪种类型的对象。要是无法接受这些限制,就可以使用泛型方法(generic method,见第27条)或者有限制的通配符类型(bounded wildcard type,见第28条)。
不要在新代码中使用原生态类型,这条规则有两个小小的例外,两者都源于“泛型信息可以在运行时被擦除”(见第25条)这一事实。在类文字(class literal)中必须使用原生态类型。规范不允许使用参数化类型(虽然允许数组类型和基本类型)[JLS,15.8.2]。换句话说,List.class
,String[].class
和int.class
都合法,但是List<String.class>
和List<?>.class
则不合法。
这条规则的第二个例外与instanceof
操作符有关。由于泛型信息可以在运行时被擦除,因此在参数化类型而非无限制通配符类型上使用instanceof
操作符是非法的。用无限制通配符类型代替原生态类型,对instanceof
操作符的行为不会产生任何影响。在这种情况下,尖括号(<>
)和问号(?
)就显得多余了。下面是利用泛型来使用instanceof
操作符的首选方法:
// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> m = (Set<?>) o; // Wildcard type
...
}
注意,一旦确定这个o
是个Set
,就必须将它转换成通配符类型Set<?>
,而不是转换成原生态类型Set
。这是个受检的(checked)转换,因此不会导致编译时警告。
总之,使用原生态类型会在运行时导致异常,因此不要在新代码中使用。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。让我们做个快速的回顾:Set<Object>
是个参数化类型,表示可以包含任何对象类型的一个集合;Set<?>
则是一个通配符类型,表示只能包含某种未知对象类型的一个集合;Set
则是个原生态类型,它脱离了泛型系统。前两种是安全的,最后一种不安全。
为便于参考,表 5-1 概括了本条目中所介绍的术语(及本章其他条目中介绍的一些术语):
表5-1 本章条目中所介绍的术语
术语 | 示例 | 所在条目 |
---|---|---|
参数化的类型 | List<String> |
第23条 |
实际类型参数 | String |
第23条 |
泛型 | List<E> |
第23,26条 |
形式类型参数 | E |
第23条 |
无限制通配符类型 | List<?> |
第23条 |
原生态类型 | List |
第23条 |
有限制类型参数 | <E extends Number> |
第26条 |
递归类型参数 | <T extends Comparable<T>> |
第27条 |
有限制通配符类型 | List<? extends Number> |
第28条 |
泛型方法 | static <E> List<E> asList(E[] a) |
第27条 |
类型令牌 | String.class |
第29条 |