第28条:列表优先于数组

Arrays differ from generic types in two important ways. First, arrays are covariant. This scary-sounding word means simply that if Sub is a subtype of Super, then the array type Sub[] is a subtype of the array type Super[]. Generics, by contrast, are invariant: for any two distinct types Type1 and Type2, List<Type1> is neither a subtype nor a supertype of List<Type2> [JLS, 4.10; Naftalin07, 2.5]. You might think this means that generics are deficient, but arguably it is arrays that are deficient. This code fragment is legal:

数组与泛型类型的不同体现在两个方面。首先,数组是协变的(covariant)。这个词看起来有点吓人,不过它也只是意味着,如果Sub是Super的一个子类型,那么数组类型Sub[]也是数组类型Super[]的子类型。相反,泛型是有约束的:对于任意的两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类也不是它的父类[JLS, 4.10; Naftalin07, 2.5]。你可能想,这是不是意味着泛型是有缺陷的,但实际上,可以说数组才是有缺陷的。下面的代码片段是合法的:

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

but this one is not:

但下面这个则不是:

// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

Either way you can’t put a String into a Long container, but with an array you find out that you’ve made a mistake at runtime; with a list, you find out at compile time. Of course, you’d rather find out at compile time.

任意一种方式你都无法将一个String放入一个Long容器里,但如果用数组的话你会在运行时才发现你犯了个错误,而如果用List,你就会在编译时发现它。当然,你会更想在编译时就发现问题。

The second major difference between arrays and generics is that arrays are reified [JLS, 4.7]. This means that arrays know and enforce their element type at runtime. As noted earlier, if you try to put a String into an array of Long, you’ll get an ArrayStoreException. Generics, by contrast, are implemented by erasure [JLS, 4.6]. This means that they enforce their type constraints only at compile time and discard (or erase) their element type information at runtime. Erasure is what allowed generic types to interoperate freely with legacy code that didn’t use generics (Item 26), ensuring a smooth transition to generics in Java 5.

数组和泛型的第二个主要区别是,数组是具化的[JLS, 4.7]。这意味着,数组在运行时才知道并检查元素类型。如前面所说,如果你将一个String放入Long数组里,你将会得到一个ArrayStoreException异常。想法,泛型则是通过擦除来实现的[JLS, 4.6]。这意味着泛型仅在编译时进行类型约束的检查并且在运行是忽略(或擦除)元素类型。擦除机制使得泛型类型可以自由地与那些未使用泛型(条目26)的遗留代码互用,确保平滑转换到Java 5的泛型。

Because of these fundamental differences, arrays and generics do not mix well. For example, it is illegal to create an array of a generic type, a parameterized type, or a type parameter. Therefore, none of these array creation expressions are legal: new List<E>[], new List<String>[], new E[]. All will result in generic array creation errors at compile time.

由于这些根本上的区别,数组和泛型无法很好地混合使用。例如,创建一个泛型类型的数组,参数化类型的数组,或者类型参数的数组,都是不合法的。因而,这些数组创建表达式都不是合法的:new List<E>[], new List<String>[], new E[]。这些表达式都会导致运行时出现泛型数组创建错误。

Why is it illegal to create a generic array? Because it isn’t typesafe. If it were legal, casts generated by the compiler in an otherwise correct program could fail at runtime with a ClassCastException. This would violate the fundamental guarantee provided by the generic type system.

为什么创建泛型数组是不合法的?因为这么做不是类型安全的。如果这么做是合法的,编译器在其它正确的程序里生成的强转就会在运行时失败,并抛出ClassCastException异常。这将违背了泛型类型系统提供的基本保证。

To make this more concrete, consider the following code fragment:

为了更具体地说明这个问题,考虑下面的代码片段:

// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)

Let’s pretend that line 1, which creates a generic array, is legal. Line 2 creates and initializes a List<Integer> containing a single element. Line 3 stores the List<String> array into an Object array variable, which is legal because arrays are covariant. Line 4 stores the List<Integer> into the sole element of the Object array, which succeeds because generics are implemented by erasure: the runtime type of a List<Integer> instance is simply List, and the runtime type of a List<String>[] instance is List[], so this assignment doesn’t generate an ArrayStoreException. Now we’re in trouble. We’ve stored a List<Integer> instance into an array that is declared to hold only List<String> instances. In line 5, we retrieve the sole element from the sole list in this array. The compiler automatically casts the retrieved element to String, but it’s an Integer, so we get a ClassCastException at runtime. In order to prevent this from happening, line 1 (which creates a generic array) must generate a compile-time error.

第一行创建了一个泛型数组,我们假设它是合法的。第二行创建并初始化了一个List<Integer>,它只包含了一个元素。第三行将List<String>数组存储到Object数组里,这么做是可以的,因为泛型使用通过擦除来实现的:List<Integer>实例的运行时类型实际上是List,List<String>[]实例的运行时类型实际上是List[],所以这条赋值语句不会生成ArrayStoreException异常。现在我们遇到一个麻烦。我们在一个声明仅包含List<String>实例的数组里存储了一个List<Integer>实例。在第5行,我们从数组里唯一的列表里取出唯一的元素。编译器自动地将获取到的元素强转为String,但它是个Integer,所以我们在运行时就得到了一个ClassCastException异常。为了防止这个现象发生,第一行(创建了泛型数组的那行)必须生成一个编译时错误。

Types such as E, List<E>, and List<String> are technically known as nonreifiable types [JLS, 4.7]. Intuitively speaking, a non-reifiable type is one whose runtime representation contains less information than its compile-time representation. Because of erasure, the only parameterized types that are reifiable are unbounded wildcard types such as List<?> and Map<?,?> (Item 26). It is legal, though rarely useful, to create arrays of unbounded wildcard types.

诸如E,List<E>,和List<String>这些类型在技术上都被称为不可具化类型[JLS, 4.7]。直观点讲,不可具化类型就是运行时展示信息比编译时展示信息要少的类型。由于擦除的原因,唯一的可具化参数化类型是诸如List<?>和Map<?,?>子类的无限制通配符类型(条目26)。创建无限制通配符类型数组虽然很少用,但却是合法的。

The prohibition on generic array creation can be annoying. It means, for example, that it’s not generally possible for a generic collection to return an array of its element type (but see Item 33 for a partial solution). It also means that you get confusing warnings when using varargs methods (Item 53) in combination with generic types. This is because every time you invoke a varargs method, an array is created to hold the varargs parameters. If the element type of this array is not reifiable, you get a warning. The SafeVarargs annotation can be used to address this issue (Item 32).

禁止创建泛型数组可能会让你觉得有点讨厌。例如,这意味着泛型集合一般都不大可能返回它的元素类型的数组(部分解决方案请见条目33)。而且还意味着,当你结合可变参数方法(条目53)和泛型类型一起使用时,你将会得到一些令人困惑的警告。这是因为,每次你调用一个可变参数方法时,Java都会创建一个数组用来存储可变参数的每个值。如果这个数组的元素类型是不可具化的,那么你将得到一个警告。SafeVarargs注解可以用来解决这个问题(条目32)。

When you get a generic array creation error or an unchecked cast warning on a cast to an array type, the best solution is often to use the collection type List<E> in preference to the array type E[]. You might sacrifice some conciseness or performance, but in exchange you get better type safety and interoperability.

当你强转成数组类型时,若得到一个泛型数组创建错误或者未检查强转警告,最好的解决办法是,总是优先采用集合类型List<E>,而不是数组类型E[]。也许你会牺牲一点性能,但你获得了更好的类型安全性和互用性。

For example, suppose you want to write a Chooser class with a constructor that takes a collection, and a single method that returns an element of the collection chosen at random. Depending on what collection you pass to the constructor, you could use a chooser as a game die, a magic 8-ball, or a data source for a Monte Carlo simulation. Here’s a simplistic implementation without generics:

例如,假设你想编写一个Chooser类,它的构造器需要接收一个集合,然后它还有一个方法会随机返回集合的某个元素。由于你可以往构造器里传入不同的集合,所以你可以用Chooser对象作为游戏模型,作为魔术8球,或者作为蒙特卡洛模拟的数据源。下面是在不采用泛型的情况下的一个简单实现:

// Chooser - a class badly in need of generics!
public class Chooser {
    private final Object[] choiceArray;
    public Chooser(Collection choices) {
        choiceArray = choices.toArray();
    } 
    public Object choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceArray[rnd.nextInt(choiceArray.length)];
    }
}

To use this class, you have to cast the choose method’s return value from Object to the desired type every time you use invoke the method, and the cast will fail at runtime if you get the type wrong. Taking the advice of Item 29 to heart, we attempt to modify Chooser to make it generic. Changes are shown in boldface:

为了正确使用这个类,每次你调用choose方法时都要将这个方法返回的值从Object类型强转为所需的类型,如果你获得的本来就是错误的类型,那么在运行时强转还会失败。根据条目29的建议,我们可以试着将Chooser泛型化。修改部分用黑体标识:

// A first cut at making Chooser generic - won't compile
public class Chooser<T> {
    private final T[] choiceArray;
    public Chooser(Collection<T> choices) {
        choiceArray = choices.toArray();
    }// choose method unchanged
}

If you try to compile this class, you’ll get this error message:

如果你尝试着编译这个类,你将获得一个错误信息:

Chooser.java:9: error: incompatible types: Object[] cannot be converted to T[]
choiceArray = choices.toArray(); 
                      ^
where T is a type-variable:
T extends Object declared in class Chooser

No big deal, you say, I’ll cast the Object array to a T array:

你可能会说,没事,我可以将Object数组强转成T数组:

choiceArray = (T[]) choices.toArray();

This gets rid of the error, but instead you get a warning:

这么做的话错误是去除了,但你会得到一个警告:

Chooser.java:9: warning: [unchecked] unchecked cast 
choiceArray = (T[]) choices.toArray();
                            ^
required: T[], found: Object[]
where T is a type-variable:
T extends Object declared in class Chooser

The compiler is telling you that it can’t vouch for the safety of the cast at runtime because the program won’t know what type T represents—remember, element type information is erased from generics at runtime. Will the program work? Yes, but the compiler can’t prove it. You could prove it to yourself, put the proof in a comment and suppress the warning with an annotation, but you’re better off eliminating the cause of warning (Item 27). To eliminate the unchecked cast warning, use a list instead of an array. Here is a version of the Chooser class that compiles without error or warning:

编译器正试图告诉你它无法保证运行时强转的安全性,因为程序无法知道T代表什么类型—记住,元素类型信息在运行时从泛型里擦除了。那这段程序能工作吗?可以,但编译器无法证明这点。你可以向你自己证明它可以跑起来,在注释里写上证明同时用注解来禁止警告,但最好还是消除引起警告的原因(条目27)。为了消除这个未检查强转警告,可以用列表,而不是数组。下面这个版本的Chooser类在编译时就不会产生错误或者警告:

// List-based Chooser - typesafe
public class Chooser<T> {
    private final List<T> choiceList;
    public Chooser(Collection<T> choices) { 
        choiceList = new ArrayList<>(choices);
    }
    public T choose() {
        Random rnd = ThreadLocalRandom.current();
        return choiceList.get(rnd.nextInt(choiceList.size()));
    } 
}

This version is a tad more verbose, and perhaps a tad slower, but it’s worth it for the peace of mind that you won’t get a ClassCastException at runtime.

这个版本的代码可能有点冗长,也许还会有点慢,但你就不要担心在运行时得到ClassCastException异常。

In summary, arrays and generics have very different type rules. Arrays are covariant and reified; generics are invariant and erased. As a consequence, arrays provide runtime type safety but not compile-time type safety, and vice versa for generics. As a rule, arrays and generics don’t mix well. If you find yourself mixing them and getting compile-time errors or warnings, your first impulse should be to replace the arrays with lists.

总之,数组和泛型有着不同的类型规则。数组是协变的并可具化的;泛型是受约束并且可擦除的。因此,数组提供了运行时类型安全性但不保证编译时类型安全性,泛型则反过来。通常,数组和泛型不能很好混用。如果你发现你混用了它们而且得到编译时错误或警告,你的第一反应应该是用列表替代数组。

results matching ""

    No results matching ""