Effective Java 3rd 条目28 列表优于队列

队列在两个方向上与泛型不相同。首先,队列是协变的。这个听上去令人不安的单词意思仅仅是:如果Sub是Super的一个子类,那么队列类型Sub[]是Super[]的一个子类。相反,泛型是非协变的:对于任何两个不同类型Type1和Type2,List<Type1>既不是List<Type2>的一个子类,也不是它的超类[JLS, 4.10; Naftalin07, 2.5]。你可能认为这意味着,泛型是有缺陷的,然而大概是队列有缺陷的。以下代码片段是合法的:

// 运行时失败!
Object[] objectArray = new Long[1]; 
objectArray[0] = "I don't fit in"; // 抛出ArrayStoreException

但是如下不是合法的:

// 编译不通过!

List<Object> ol = new ArrayList<Long>(); // 不可兼容类型 
ol.add("I don't fit in");

两种方式都不能把String放到Long容器里面,但是使用队列,你会在运行时发现一个错误;使用列表,你会在编译时发现一个错误。当然,你应该宁愿在编译时发现错误。
队列和泛型之间第二个主要区别是,队列是具体化的[JLS, 4.7]。这意味着,队列在运行时知道和约束了它们的元素类型。就像以前提到的,如果你试着把一个String到一个Long队列中,你将得到ArrayStoreException。相反,泛型是实施为类型擦除的[JLS, 4.6]。这意味着,它们仅仅在编译时约束了它们的类型,而且在允许时抛弃了(擦除了)它们的元素类型信息。擦除使得泛型类型和不使用泛型的遗留代码自由地互操作(条目26),这保证了在Java5中顺利地过度到泛型。

因为这些基本的不同,队列和泛型不能够很好地混合使用。例如,创建一个泛型类型的、参数化类型的或者类型参数的队列是不合法的。所以,这些队列创建表达式都不是合法的:new List<E>[], new List<String>[], new E[]。所有这些都会导致编译时泛型队列创建错误。

创建泛型队列为什么是不合法的呢?因为它不是类型安全的。如果它是合法的,在其他正确程序中,编译器产生的强转可能在运行时以ClassCastException方式失败了。这违反了泛型类型系统提供的基本保证。

为了使得更加具体,考虑如下代码片段:

// 泛型队列创建为什么是不合法的 - 编译不通过! 
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)

让我们假设,第一行,创建了泛型队列,是合法的。第二行创建和初始化了一个List<Integer>,它包含了单一元素。第三行保存List<String>队列到Object队列变量中,这是合法的,因为队列是协变的。第四行保存 List<Integer> 到Object队列的单个元素中,这是可以成功的,因为泛型是实现擦除的:List<Integer>实例的运行类型仅仅是List,而且List<String>[]实例的运行类型是List[],所以这个赋值不会产生ArrayStoreException。现在我们有麻烦了。我们保存了List<Integer>实例到一个声明为仅仅存储List<String>实例的列表中。在第五行,我们从这个队列的单个列表中取得单个元素。编译器自动强转取到的元素到String,但是它是一个Integer,所以,我们在运行时获得ClassCastException。为了阻止这个发生,第一行(它创建了泛型队列)必须产生一个编译时错误。

像E, List<E>, and List<String>类型技术上被认为是不合具体化的类型[JLS, 4.7]。直观上来说,不可具体化类型是这样的类型,相对于编译时表示,它的运行时表示包含了更少的信息。因为擦除,可具体化的唯一参数化类型是像List<?>和Map<?,?>这样的非受限通配符类型(条目26)。创建非受限通配符类型的队列是合法的,虽然极少使用。

禁止泛型队列创建可能很恼人。这意味着,例如,泛型集合返回一个它的元素类型的队列,这通常是不可能的(但是为部分解决方案参考条目33)。这也意味着,当使用varargs方法(条目53),与泛型类型结合,你将得到令人困惑的警告。如果这个队列的元素类型是不可具体化的,那么你将得到一个警告。SafeVarargs注解可以使用在解决这个问题(条目32)。

当你得到一个泛型队列创建错误或者一个对于队列类型强转的非受检强转警告,最好的解决方案是经常使用结合类型List<E>,而不是队列类型E[]。你可能牺牲简明或者性能,但是作为交换,你获得更好的类型安全和互操作性。

例如,假设你想要编写一个具有接受集合构造子和一个返回随机选择集合的元素的单个方法的Chooser类 。取决于你传入到构造子的何种集合,你可能使用chooser作为一个游戏骰子、魔法8球或者蒙地卡罗模拟器的数据来源。以下是一个没有泛型的简单实现:

// Chooser - 一个极其需要泛型的类! 
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类的版本,编译时没有错误或者警告:

// 基于列表的Chooser - 安全类型
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阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容