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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,132评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,457评论 3 404
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,411评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,147评论 1 301
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,145评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,611评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,962评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,948评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,479评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,514评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,640评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,228评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,973评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,402评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,551评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,210评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,714评论 2 366

推荐阅读更多精彩内容