ITEM 26: 不要使用不带类型信息的原始泛型
ITEM 26: DON’T USE RAW TYPES
首先是一些术语。声明有一个或多个类型参数的类或接口是 泛型类或接口[JLS, 8.1.2, 9.1.2]。例如,List 接口只有一个类型参数 E,表示它的元素类型。接口的全称是 List<E> (读作“List of E”),但人们通常简称它为 List。泛型类和接口统称为泛型类型。
每个泛型类型定义一组参数化类型,这些参数化类型由类名或接口名以及泛型类型的形式化类型参数对应的实际类型参数的角括号列表组成[JLS, 4.4, 4.5]。例如,List<String> (读作“List of String”) 是一个参数化类型,表示元素为 String 类型的列表。(String是与形式类型参数e对应的实际类型参数)
最后,每个泛型类型定义一个原始类型,这是使用的泛型类型的名称,没有任何伴随的类型参数[JLS, 4.8]。例如,List<E> 对应的原始类型是List。原始类型的行为就好像从类型声明中删除了所有泛型类型信息一样。它们的存在主要是为了与预泛型代码兼容。
在泛型被添加到Java之前,这是一个典型的集合声明。从Java 9开始,它仍然是合法的,但远非典范:
// Raw collection type - don't do this!
// My stamp collection. Contains only Stamp instances.
private final Collection stamps = ... ;
如果您今天使用此声明,然后不小心将硬币放入邮票收集中,则错误的插入将编译并不会发出警告(尽管编译器确实发出一个模糊的警告):
// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... )); // Emits "unchecked call" warning
你不会得到一个错误,直到你试图从邮票收集:
// Raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hasNext(); ) {
Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
stamp.cancel();
}
正如在本书中所提到的,在错误发生后尽快发现错误是值得的,最好是在编译时。在本例中,直到运行时(在错误发生很久之后),以及在可能与包含错误的代码相距较远的代码中,您才会发现错误。一旦看到 ClassCastException,就必须在代码基中搜索将硬币放入邮票集合的方法调用。编译器无法帮助您,因为它无法理解“只包含Stamp实例”的注释。
对于泛型,应当让类型声明包含信息,而不是注释:
// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ... ;
从这个声明中,编译器知道戳记应该只包含戳记实例,并保证它是正确的,假设您的整个代码库在编译时没有发出(或抑制; item 27)任何警告。当使用参数化类型声明声明戳记时,错误插入将生成编译时错误消息,准确地告诉您错误是什么:
“Test.java:9: error: incompatible types: Coin cannot be converted to Stamp c.add(new Coin())”;
当从集合中检索元素时,编译器会为您插入不可见的强制转换,并确保它们不会失败(同样,假设您的所有代码都没有生成或禁止任何编译器警告)。虽然意外地将一枚硬币投入邮票收藏的可能性似乎有些牵强,但问题确实存在。例如,很容易想象将 BigInteger 放入一个集合中,这个集合应该只包含BigDecimal实例。
如前所述,使用原始类型(没有类型参数的泛型类型)是合法的,但是您不应该这样做。如果使用原始类型,就会失去泛型的所有安全性和表达性优势。既然您不应该使用它们,那么为什么语言设计器首先允许原始类型呢?这是为了兼容性。当添加泛型时,Java即将进入第二个十年,并且存在大量不使用泛型的代码。所有这些代码都必须保持合法,并与使用泛型的新代码进行互操作,这一点被认为是至关重要的。将参数化类型的实例传递给设计用于原始类型的方法必须是合法的,反之亦然。这一需求称为迁移兼容性,它促使决策支持原始类型,并使用擦除实现泛型(item 28)。
虽然不应该使用 List 等原始类型,但是可以使用参数化的类型来插入任意对象,比如 List<Object>。原始类型列表和参数化类型列表之间的区别是什么?松散地说,前者选择退出泛型类型系统,而后者明确地告诉编译器它能够保存任何类型的对象。虽然可以将 List<String> 传递给List 类型的参数,但是不能将它传递给 List<Object> 类型的参数。泛型有子类型规则, List<String> 是原始类型列表的子类型,但不是参数化类型列表(item 28)的子类型。因此,如果使用原始类型(如List),就会失去类型安全性,但是如果使用参数化类型(如 List<Object>),则不会。
要使其具体化,请考虑以下程序:
// Fails at runtime - unsafeAdd method uses a raw type (List)!
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
unsafeAdd(strings, Integer.valueOf(42));
String s = strings.get(0); // Has compiler-generated cast
}
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); ”
实际上,如果您运行程序,当程序试图将调用 String. get(0) 的结果转换为字符串时,您会得到一个 ClassCastException,它是一个整数。这是编译器生成的强制转换,因此通常保证成功,但在本例中,我们忽略了编译器警告,并为此付出了代价。如果将 unsafeAdd 声明中的原始类型列表替换为参数化类型列表并尝试重新编译程序,您会发现它不再编译,而是发出错误消息:
“Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object> unsafeAdd(strings, Integer.valueOf(42)); ”
对于元素类型未知且无关紧要的集合,您可能倾向于使用原始类型。例如,假设您想编写一个方法,该方法接受两个集合并返回它们共有的元素数量。如果你是泛型的新手,你可以这样写:
// 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;
}
这种方法有效,但它使用原始类型,这是危险的。安全的替代方法是使用无界通配符类型。如果您想使用泛型类型,但不知道或不关心实际的类型参数是什么,则可以使用问号。例如,Set<E> 写作 Set<?> (读" set of some type ")。它是最通用的参数化Set type,能够保存任何集合。
// Uses unbounded wildcard type - typesafe and flexible
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<?> 中吗,并且你不能对你得到的对象的类型做任何假设。如果这些限制不可接受,可以使用泛型方法(item 30)或有界通配符类型(item 31)。
对于不应该使用原始类型的规则,有几个小的例外。必须在类文字中使用原始类型。该规范不允许使用参数化类型(尽管它允许数组类型和基本类型)[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<?> s = (Set<?>) o; // Wildcard type
...
}
注意,一旦确定o是一个 Set,就必须将其转换为通配符类型 Set<?>,而不是原始类型集。这是一个已检查的强制转换,因此不会引起编译器警告。
总之,使用原始类型会在运行时导致异常,所以不要使用它们。它们仅用于与引入泛型之前的遗留代码的兼容性和互操作性。快速回顾一下,Set<Object> 是一种参数化类型,表示可以包含任何类型的元素 。Set<?> 是一个通配符类型,它表示一个集合,该集合只能包含某些未知类型的对象,而 Set 是一个原始类型,它选择退出泛型类型系统。前两种是安全的,后一种则不是。为方便参考,本项目中介绍的术语(以及本章后面介绍的一些术语)总结如下:
![](https://img.haomeiwen.com/i15984689/1bd539d7c1e5d629.png)