第26项:不要使用原生态类型

  先来介绍一些术语。声明中具有一个或者多个类型参数(type parameter)的类或者接口,就是泛型(generic)类或者接口[JLS, 8.1.2, 9.1.2]。例如,List接口就只有单个类型参数E,表示列表的元素类型。接口的全名是List(读作“E的列表”),但人们通常简称为List。泛型类和接口统称为泛型类型(generic type)。

  每种泛型定义一组参数化的类型(parameterized type),构成格式为:先是类或者接口的名称,接着用尖括号把对应于泛型形式类型参数的实际类型参数列表[JLS,4.4,4.5]括起来。例如,List(读作“字符串列表”)是一个参数化的类型,表示元素类型为String的列表。(String是与形式类型参数E相对应的实际类型参数。)

  最后,每个泛型都定义一个原生态类型(raw type),即不带任何实际类型参数的泛型名称[JLS,4.8]。例如,与List对应的原生态类型是List。原生态类型就像从类型声明中删除了所有泛型信息一样。它们的存在主要是为了与之前的泛型代码兼容。

  在将泛型添加到Java之前,这是一个规范的集合声明。从Java 9开始,它仍然是合法的,但并不是很规范:

// Raw collection type - don't do this!
// My stamp collection. Contains only Stamp instances.
private final Collection stamps = ... ;

  如果你今天使用这个声明然后不小心将coin放入你的stamp几集合中,这一错误的插入照样会编译并运行并且不会出现任何错误提示(尽管编译器发出了模糊的警告):

// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... )); // Emits "unchecked call" warning

  直到从stamp集合中获取coin时才会收到错误提示:

// Raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hasNext(); )
    Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
stamp.cancel();

  就如本书中经常提到的,出错之后应该尽快发现,最好是编译时就发现。本例中,知道运行时才发现错误,已经出错很久了,而且你在代码中所处的文职距离包含错误的这部分代码已经很远了。一旦发现ClassCastException,就必须搜索代码,查找将coin放进stamp集合的方法调用。此时编译器帮不上忙,因为它无法理解这种注释:“Contains only Stamp instances(只包含stamp实例)”。

  有了泛型,就可以利用改进后的类型声明来代替集合中的这种注释,告诉编译器之前的注释中所隐含的信息:

// Parameterized collection type - typesafe
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());
        ^

  当从集合中检索元素时,编译器会为您插入不可见的强制转换,并保证它们不会失败(再次假设您的所有代码都没有生成或禁止任何编译器警告)。虽然意外地将coin插入stamp集合的假设可能看起来很牵强,但问题仍然存在。例如,很容易想象将BigInteger放入一个应该只包含BigDecimal实例的集合中。

  如上所述,如果不提供类型参数,使用集合类型和其他泛型也仍然是合法的,但是不应该这么做。如果使用原生态类型,就是掉了泛型在安全性和表述性方面的所有优势。 既然不应该使用原生态类型,为什么Java的设计者还允许使用它们呢?这是为了提供兼容性。因为泛型出现的时候,Java平台即将进入它的第二个十年,已经存在大量没有使用泛型的Java代码。人们认为让所有这些代码保持合法,并且能够与使用泛型的新代码互用,这一点很重要。它必须合法,才能将参数化类型的实例传递给那些被设计成使用普通类型的方法,反之亦然。这种需求被称作移植兼容性(Migration Compatibility),促成了支持原生态类型的决定并使用擦除(erasure)来实现泛型(第28项)。

  虽然不应该在新代码中使用像List这样的原生态类型,使用参数化的类型以允许插入任意对象,如List,这还是可以的。原生态类型List和参数化的类型List之间到底有什么区别呢?不严格地说,前者逃避了泛型检查,后者则明确告知编译器,它能够持有任意类型的对象。虽然你可以将List传递给类型List参数,但是不能将它传给类型List的参数。泛型有子类化(subtyping)的规则,List是原生态类型List的一个子类型,而不是参数化类型List的子类型(第28项)。因此,如果使用像List这样的原生态类型,就会失掉类型安全性,但是如果使用像List这样的参数化类型,则不会。

  为了更具体地进行说明,请参考下面的程序:

// 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);
}

  这个程序可以进行编译,但是因为它使用了原生态类型List,你会收到一条警告:

Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add(o);
        ^

  实际上,如果运行这段程序,在程序试图将strings.get(0)的调用结果转换成一个String时,会收到一个ClassCastException异常。这是一个编译器生成的转换,因此一般保证会成功,但是我们在这个例子中忽略了一条编译器的警告,就会为此而付出代价。

  如果在unsafeAdd声明中用参数化类型List代替原生态类型List,并试着重新编译这段程序,会发现它无法再进行编译了。以下是它的错误消息:

Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
unsafeAdd(strings, Integer.valueOf(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;
}

  这个方法倒是可以工作,但它使用了原生态类型,这是很危险的。安全的替代方法是使用无限制的通配符类型(unbounded wildcard type)。如果要使用泛型,但不确定或者不关心实际的类型参数,就可以使用一个问号代替。例如,泛型Set的无限制通配符类型为Set(读作“某个类型的集合”)。这是最普通的参数化Set类型,可以持有任何集合。下面是numElementsInCommon方法使用了无限制通配符类型时的情形:

// 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中,而且根本无法猜测你会得到哪种类型的对象。要是无法接受这些限制,就可以使用泛型方法(generic method,第30项)或者有限制的通配符类型(bounded wildcard type,第31项)。

  不要在代码中使用原生态类型,这条规则有一些小小的例外。在类文字(class literal)中必须使用原生态类型。 规范不允许使用参数化类型(虽然允许数组类型和基本类型)[JLS, 15.8.2]。换句话说,List.class,String[].class和int.class都合法,但是List和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。这是个受检的(checked)转换,因此不会导致编译时警告。

  总之,使用原生态类型会在运行时导致异常,因此不要在代码中使用它们。原生态类型只是为了与引入泛型之前的遗留代码进行兼容和互用而提供的。让我们做个快速的回顾:Set是个参数化类型,表示可以包含任何对象类型的一个集合;Set则是一个通配符类型,表示只能包含某种位置对象类型的一个集合;Set则是个原生态类型,它脱离了泛型系统。前两种是安全的,最后一种不安全。

  为了便于快速参考,本项中介绍的术语(以及本章后面介绍的一些术语)总结在下表中:

术语 示例 所在项
参数化的类型 List 第26项
实际类型参数 String 第26项
泛型 List 第26、29项
形式类型参数 E 第26项
无限制通配符类型 List 第26项
原生态类型 List 第26项
有限制类型参数 第29项
递归类型限制 第30项
有限制通配符类型 List 第31项
泛型方法 static List asList(E[] a) 第30项
类型令牌 String.class 第33项

第27项:消除非受检警告

你可能感兴趣的:(Effective,Java,第三版翻译)