第25条:列表优先于数组

数组与泛型相比,有两个重要的不同点。首先,数组是协变的(covariant)。这个词听起来有点吓人,其实只是表示如果SubSuper的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant):对于任意两个不同的类型Type1Type2List<Type1>既不是List<Type2>的子类型,也不是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

但下面这段代码则不合法;

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

这其中无论哪种方法,都不能将String放进Long容器中,但是利用数组,你会在运行时发现所犯的错误;利用列表,则可以在编译时发现错误。我们当然希望在编译时发现错误了。

数组与泛型之间的第二大区别在于,数组是具体化的(reified)[JLS,4.7]。因此数组会在运行时才知道并检查它们的元素类型约束。如上所述,如果企图将String保存到Long数组中,就会得到一个ArrayStoreException异常。相比之下,泛型则是通过擦除(erasure)[JLS,4.6]来实现的。因此泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用(见第23条)。

由于上述这些根本的区别,因此数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。这些数组创建表达式没有一个是合法的:new List<E>[]new List<String>[]new E[]。这些在编译是都会导致一个 generic array creation(泛型数组创建)错误。

为什么创建泛型数组是非法的?因为它不是类型安全的。要是它合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个ClassCastException异常。这就违背了泛型系统提供的基本保证。

为了更具体地对此进行说明,考虑以下代码片段:

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

我们假设第1行是合法的,它创建了一个泛型数组。第2行创建并初始化了一个包含单个元素的List<Integer>。第3行将List<String>数组保存到一个Object数组变量中,这是合法的,因为数组是协变的。第4行将List<Integer>保存到Object数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的:List<Integer>实例的运行时类型只是ListList<String>[]实例的运行时类型则是List[],因此这种安排不会产生ArrayStoreException异常。但现在我们有麻烦了。我们将一个List<Integer>实例保存到了原本声明只包含List<String>实例的数组中。在第5行中,我们从这个数组里唯一的列表中获取了唯一的元素。编译器自动地将获取到的元素转换成String,但它是一个Integer,因此,我们在运行时得到了一个ClassCastExcption异常。为了防止出现这种情况,(创建泛型数组的)第1行产生了一个编译时错误。

从技术的角度来说,想EList<E>List<String>这样的类型应称作不可具体化的(nonreifiable)类型[JLS,4.7]。直观地说,不可具体化的(non-reifiable)类型是指其运行时表示法包含的信息比它的编译时表示法包含的信息更少的类型。唯一可具体化的(reifiable)参数化类型是无限制的通配符类型,如List<?>Map<?,?>(见第23条)。虽然不常用,但是创建无限制通配符类型的数组是合法的。

禁止创建泛型数组可能有点讨厌。例如,这表明泛型一般不可能返回它的元素类型数组(部分解决方案请见第29条)。这也意味着在结合使用可变参数(varargs)方法(见第42条)和泛型时会出现令人费解的警告。这是由于每当调用可变参数方法时,就会创建一个数组来存放 varargs 参数。如果这个数组的元素类型时不是可具体化的(reifiable),就会得到一条警告。关于这些警告,除了把它们禁止(见第24条),并且避免在API中混合使用泛型与可变参数之外,别无他法。

当你得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或者简洁性,但是换回的却是更高的类型安全性和互用性。

例如,假设有一个(Collections.synchronizedList返回的那种)同步列表和一个函数(它有两个域该列表的元素同类型的参数值,并返回第三个值)。现在假设要编写一个方法reduce,并使用函数apply来处理这个列表。假设列表元素类型为整数,并且函数是用来做两个整数求和运算,reduce方法就会返回列表中所有值的总和。如果函数是用来做两个整数求积的运算,该方法就会返回列表中值的乘积。如果列表包含字符串,并且函数连接两个字符串,该方法就会返回一个字符串,它按顺序包含了列表中的所有字符串。除了列表和函数之外,reduce方法还采用初始值进行减法运算,列表为空时会返回这个初始值。(初始值一般为函数的识别元素,加法为0,乘法为1,字符串连接时是""。)以下是没有泛型时的代码?

// Reduction without generics, and with concurrency flaw!
static Object reduce(List list, Function f, Object initVal) {
    synchronized(list) {
        Object result = initVal;
        for (Object o : list)
            result = f.apply(result, o);
        return result;
    }
}

interface Function {
    Object apply(Object arg1, Object arg2);
}

假设你现在已经读过第67条,它告诉你不要从同步区域中调用“外来的(alien)方法”。因此,在持有锁的时候修改reduce方法来复制列表中内容,也可以让你在备份上执行减法。Java 1.5 发行版本之前,要这么做一般是利用ListtoArray方法(它在内部锁定列表):

// Reduction without generics or concurrency flaw
static Object reduce(List list, Function f, Object initVal) {
    Object[] snapshot = list.toArray(); // Locks list internally
    Object result = initVal;
    for (E e : snapshot)
        result = f.apply(result, e);
    return result;
}

如果试图通过泛型来完成这一点,就会遇到我们之前讨论过的那种麻烦。以下是Function接口的泛型版:

interface Function<T> {
    T apply(T arg1, T arg2);
}

下面是一种天真的尝试,试图将泛型应用到修改过的reduce方法。这是一个泛型方法(generic method,见第27条)。如果你不理解这条声明,也不必担心。对于这个条目来说,应该把注意力集中在方法体上:

// Naive generic version of reduction - won't compile!
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
    E[] snapshot = list.toArray(); // Locks list
    E result = initVal;
    for (E e : snapshot) 
        result = f.apply(result, e);
    return result;
}

如果试着编译这个方法,就会得到下面的错误消息:

Reduce.java:12: incompativle types
found   : Object[], required: E[]
        E[] snapshot = list.toArray(); // Locks list
                               ^

你会说,这没什么大不了的,我会将Object数组转换成一个E数组:

E[] snapshot = (E[]) list.toArray();

它是消除了那条错误,但是现在得到了一条警告:

Reduce.java:12: warning: [unchecked] unchecked cast
found   : Object[], required:L E[]
        E[] snapshot = (E[]) list.toArray(); // Locks list
                                         ^

编译器告诉你,它无法在运行时检查转换的安全性,因为它在运行时还不知道E是什么——记住,元素类型信息会在运行时从泛型中被擦除。这段程序可以运行吗?结果表明,它可以运行,但是不安全。通过微小的修改,就可以让它在没有包含显式转换的行上抛出ClassCastException异常。snapshot的编译时类型为E[],它可以为String[]Integer[]或者任何其他种类的数组。运行时类型为Object[],这是很危险的。不可具体化的类型的数组转换只能在特殊情况下使用(见第26条)。

那么应该做些什么呢?用列表代替数组。下面的reduce方法编译时就没有任何错误或者警告:

// List-based generic reduction
static <E> E reduce(List<E> list, Funciton<E> f, E initVal) {
    List<E> snapshot;
    synchronized(list) {
        snapshot = new ArrayList<E>(list);
    }
    E result = initVal;
    for (E e : snapshot)
        result = f.apply(result, e);
    return result;
}

这个版本的代码比数组版的代码稍微冗长一点,但是可以确定在运行时不会得到ClassCastException异常,为此也值了。

总而言之,数组和泛型有着非常不同的类型规则。数组是协变并且可以具体化的;反省是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时的错误或者警告,你的第一反应就应该是用列表代替数组。

results matching ""

    No results matching ""