项目28: 优先考虑使用列表而不是数组

2019-06-07  本文已影响0人  rabbittttt

ITEM 28: PREFER LISTS TO ARRAYS
  数组与泛型类型有两个重要区别。首先,数组是协变的。这个听起来很吓人的单词的意思很简单,如果 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

  但下面这个不是:

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

  无论哪种方法,你都不能将 String 放入一个 Long 的容器中,但是使用数组,你会发现你在运行时犯了一个错误;使用列表,您可以在编译时找到。当然,您更愿意在编译时找到答案。
  数组和泛型的第二个主要区别是数组是具体化的[JLS, 4.7]。这意味着数组在运行时知道并执行它们的元素类型。如前所述,如果尝试将 String 放入Long 数组中,将得到 ArrayStoreException。相比之下,泛型是通过擦除实现的[JLS, 4.6]。这意味着它们只在编译时执行类型约束,并在运行时丢弃(或擦除)它们的元素类型信息。擦除允许泛型类型与不使用泛型的遗留代码自由地互操作(第26项),确保平稳地过渡到Java 5中的泛型。
  由于这些基本差异,数组和泛型不能很好地混合。例如,创建泛型类型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都不合法: new List[]、new List[]、new E[]。所有这些都会在编译时导致泛型数组创建错误。
  为什么创建泛型数组是非法的?因为它不是类型安全。如果它是合法的,编译器在其他正确的程序中生成的强制转换可能在运行时失败,出现 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行创建并初始化一个包含单个元素的列表。第3行将列表数组存储到Object array变量中,这是合法的,因为数组是协变的。4号线存储列表<整数>为唯一对象数组的元素,通过擦除成功因为实现泛型:<整数>列表实例的运行时类型是简单的列表,列表和运行时类型的<字符串>[][]实例列表,所以这个任务不产生 ArrayStoreException。现在我们有麻烦了。我们将一个List实例存储到一个数组中,该数组声明为只包含List实例。在第5行中,我们从这个数组的sole列表中检索sole元素。编译器自动将检索到的元素转换为String,但它是一个整数,因此我们在运行时获得一个 ClassCastException。为了防止这种情况发生,第1行(创建泛型数组)必须生成编译时错误。
  像 E、List<E> 和 List<String> 这样的类型在技术上被称为不可具体化类型[JLS, 4.7]。直观地说,非具体化类型是指运行时表示包含的信息少于编译时表示。由于擦除,惟一可具体化的参数化类型是无界通配符类型,如 List<?> 和 Map<?,?> (item 26)。创建无界通配符类型数组虽然很少有用,但却是合法的。
  禁止创建泛型数组可能很烦人。例如,这意味着泛型集合通常不可能返回其元素类型的数组(但部分解决方案请参见第33项)。这还意味着,当将 varargs 方法(第53项)与泛型类型结合使用时,您会得到混淆的警告。这是因为每次调用 varargs 方法时,都会创建一个数组来保存 varargs 参数。如果该数组的元素类型不可具体化,则会得到警告。
SafeVarargs 注释可以用来解决这个问题(第32项)。
  当对数组类型进行强制转换时,得到一个泛型数组创建错误或未选中的强制转换警告时,最好的解决方案通常是使用集合类型 List,而不是数组类型 E[]。您可能会牺牲一些简洁性或性能,但作为交换,您可以获得更好的类型安全性和互操作性。
  例如,假设您想编写一个 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)];
  } 
}

  要使用这个类,每次使用invoke方法时都必须将choose方法的返回值从对象转换为所需的类型,如果类型错误,则在运行时转换将失败。考虑到第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 ”
  没什么大不了的,你说,我将把对象数组转换成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项)。
若要消除未选中的强制转换警告,请使用列表而不是数组。这是一个版本的选择类编译没有错误或警告:

// 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。
  总之,数组和泛型有非常不同的类型规则。数组是协变和具体化的;泛型是不变的并被擦除。因此,数组提供运行时类型安全,但不提供编译时类型安全,泛型亦然。通常,数组和泛型不能很好地混合。如果您发现自己混合了它们并得到编译时错误或警告,您的第一个行为应该是用列表替换数组。

上一篇下一篇

猜你喜欢

热点阅读