第28项:列表优先于数组

  数组与泛型相比,有两个重要的不同点。首先,数组是协变的(covariant)。这个词听起来有点吓人,其实只是表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant):对于任意两个不同的类型Type1和Type2,List既不是List的子类型,也不是List的超类型[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<Long>(); // Incompatible types
ol.add("I don't fit in");

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

  数组与泛型之间的第二大区别在于,数组是具体化的(reified)[JLS, 4.7]。因此数组会在运行时才知道并检查它们的元素类型约束。如上所述,如果你企图将String保存到Long数组中,就会的得到一个ArrayStoreException异常。相比之下,泛型则是通过擦除(erasure)[JLS, 4.6]来实现的。因此泛型只是在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除是允许泛型类型与不使用泛型的遗留代码(第26项)自由互操作的原因,确保在Java 5中平滑过渡到泛型。

  由于上述这些根本的区别,因此数组和泛型不能很好地混合使用。例如,创建泛型、参数化类型或者类型参数的数组是非法的。这些数组创建表达式没有一个是合法的: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 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)

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

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

  禁止创建泛型数组可能有点讨厌。例如,这表明泛型一般不可能返回它的元素类型数组(部分解决方案请见第33项)。这也意味着在结合使用可变参数(varargs)方法(见第53项)和泛型时会出现令人费解的警告。这是由于每当调用可变参数方法时,就会创建一个数组来存放varargs参数,如果这个数组的元素类型不是可具体化的(reifialbe),就会得到一条警告。SafeVarargs注释可用于解决此问题(第32项)。

  当你在转换为数组类型时获得泛型数组创建错误或未经检查的强制转换警告时,最佳解决方案通常是优先使用集合类型List而不是数组类型E[]。你可能会牺牲一些简洁性或性能,但是换回的却是更高的类型安全性和互用性。

  例如,假设你要编写一个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)];
    }
}

  要使用此类,每次使用调用方法时都必须将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
}

  如果你尝试编译这个类,你会得到这样的错误信息:

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

  没什么大不了,你会说,我会将Object数组转换为T数组:

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

  这摆脱了错误,但你得到一个警告:

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

  编译器告诉你它无法在运行时保证强制转换的安全性,因为程序不知道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()));
    }
}

  这个版本有点冗长,也许有点慢,但是为了让你高枕无忧,你不会在运行时获得ClassCastException异常。

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

第29项:优先考虑泛型

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值