Effective Java(3rd)-Item28 推荐使用列表而不是数组

  数组和泛型有两种重要的区别。首先,数组是协变的。这个听起来吓人的单词简单地意思是,如果Sub是Super的子类,Sub[]数组的类型也是Super[]数组的类型的子类。泛型,相反,是不可变的:对于任何确定的类型Type1和Type2,List既不是List的子类又不是父类 [JLS, 4.10; Naftalin07, 2.5]。你可能会认为泛型有缺点,但是恰恰说明是数据有不足。如下代码片段是合法的:

image.png

这一段是不合法的:
image.png

  无论那种方式,你都不能将字符串放入一个Long的容器中,但是使用数据,你会在运行时发生错误;使用列表,你就可以在编译时间找出,当然,你宁愿在编译时发现。
  数组和泛型的第二个主要区别是数组是具体化的 [JLS, 4.7]。这意味着数组在运行时知道并强制它们的元素类型。如前所述,如果你尝试向一个Long类型的数组放入String,你将会得到ArrayStoreException。相反,泛型是通过擦除实现的[JLS, 4.6]。这意味着它们只在编译时强制执行它们的类型约束,并在运行时丢弃(或擦除)其元素类型信息。擦除允许泛型类型与不使用泛型的遗留代码(item26) 自由操作,确保在Java5中顺利过渡到泛型。

  由于这些基本差异,数组和泛型不能很好地混在一起。比如,创建一个泛型类型,参数化类型或类型参数的数组是非法的。所以,这些创建数组表达式都是不合法的: new List[], new List[], new E[]。所有这些都会在编译时导致泛型数组创建错误。
  为什么创建泛型数组是非法的?因为它不是类型安全的。如果这样是合法的,编译器在其他正确的程序中生成的强制转换可能会在运行时抛出ClassCastException。这将违反通用类型系统提供的保证。
  为了使之更具体化,考虑如下代码片段:

Effective Java(3rd)-Item28 推荐使用列表而不是数组_第1张图片
image.png

  我们假设第一行创建泛型数组是合法的。第二行实例化了一个包含单个元素的List。第三行存储了List数组到对象数组变量中,这是合法的因为数组是协变的。第四行存储了List到Object数组的底元素中,这是成功的因为泛型基于擦除实现:List实例的运行时类型是List,而List[]实例的运行时类型是list[],因此赋值不会抛出ArrayStoreException。现在,我们陷入了麻烦。我们已经将一个List实例存储到一个数组中,该数组被声明未只包含List实例。在第五行中,我们从这个数组中唯一的列表中检索唯一的元素。编译器自动将检索到的元素转换未String,但是它是一个Integer,因此我们在运行时抛出了ClassCastException。为了放置这种情况的发生,第一行(创建泛型数组)必须产生编译时错误。

  譬如类型E,List,和List在技术上称为不可具体化类型 [JLS, 4.7].

直观地说,不可具体化类型是指其允许时表示比其编译时表示所包含的信息少的类型。因为擦除,唯一刻度的参数化类型是无界通配符类型,比如List和Map(item26) .创建无界通配符类型数组是合法的,但是很少有用。

  禁止创建泛型数组可能会很烦人。这意味着,比如,在一般情况下,泛型集合不可能返回其元素类型的数组(部分解决方案参照(item33) ).这也意味着在使用varargs方法和泛型类型时会得到令人困惑的警告 (item53) 。这是因为每次调用varargs方法时,都会创建一个数组来保存varargs参数。如果此数组的元素类型不可重用,则会收到警告。SafeVarargs注解就是来解决这个问题的(item32) 。

  当你收到对数组类型的强制转换的泛型数组创建错误或未检查的强制转换警告时,最好的解决方法通常是有限使用集合类型List,而不是数组类型E[]。你可能会牺牲一些简洁或性能,但作为交换,你可以获取更好的类型安全性和互操作性。
  比如,假设你想要编写一个有接受集合的构造方法的Chooser类,以及返回随机选择的集合的元素的单个方法。根据传递给构造方法的集合,你可以使用选择器作为游戏骰子,魔术8球或蒙特卡洛模拟的数据源。下面是没有泛型的一个简单实现:


Effective Java(3rd)-Item28 推荐使用列表而不是数组_第2张图片
image.png

  为了使用这个类,你不得不每次在调用方法的时候强制转换choose方法的返回值从Object到需要的类型,如果你的类型是错误的,强制转换可能会在运行时失败。根据item29的建议,我们尝试修改Chooser使之泛型化。变化用黑体显示:


Effective Java(3rd)-Item28 推荐使用列表而不是数组_第3张图片
image.png

  如果你尝试编译这个类,你将得到这个错误消息:


Effective Java(3rd)-Item28 推荐使用列表而不是数组_第4张图片
image.png

  没什么大不了的,你说,我会把对象数组转为T数组:
choiceArray = (T[]) choices.toArray();
  这样可以消除报错,但是却得到了一个警告:


Effective Java(3rd)-Item28 推荐使用列表而不是数组_第5张图片
image.png

  编译器告诉了你,它不能保证运行时强制转换的安全性,因为程序不知道T类型代表什么——记住,元素类型信息在运行时就从泛型中删除了。这个程序会起作用吗?当然,但是编译器不能证明这一点,你可以自己证明,把证据放在注释中,用注解来抑制警告,但是最好消除警告的原因(item27) 。若要消除未选择的强制转换警告,请使用list而不是array。以下是没有错误或警告的编译的Chooser类的版本:

Effective Java(3rd)-Item28 推荐使用列表而不是数组_第6张图片
image.png

  这个版本更冗长,也可能更慢,但是值得的是,你不会在运行时得到ClasCastException。
  总之,数组和泛型在类型规则上是非常不同的。数组是协变的和具体化的,泛型是不变的和擦除的。因此,数组提供了运行时的类型安全性,但是不提供编译时类型安全性,对于泛型,反之亦然。一般来说,数组和泛型不能很好地混合在一起。如果你发现自己混合它们并得到编译时错误或警告,那么你的一个冲动应该是用列表来替代数组。

本文写于2019.6.15,历时天

你可能感兴趣的:(Effective Java(3rd)-Item28 推荐使用列表而不是数组)