Effective Java 3rd 条目26 不要使用原生类型

首先,一些术语。类或者接口,它的声明有一个或者多个类型参数(type parameter),是泛型(generic)类或者接口[JLS, 8.1.2, 9.1.2]。例如,List接口有一个类型参数,E,代表它的元素类型。这个接口的完整名字是List<E>(读作“E的列表”),但是人们常常简单地叫它为List。泛型类和接口全部叫做泛型类型。

每个泛型类型定义了参数化(parameterized)类型的集,它包含了类或者接口名字,紧跟着实际类型参数(actual type parameter)的尖括号列表,这些类型参数是相对于泛型类型的形式类型参数[JLS, 4.4, 4.5]。例如,List<String>(读作“字符串的列表”)是一个参数化类型,代表元素是String类型的一个列表。(String 是相对于形式类型参数E的实际类型参数。)

最后,每个泛型参数定义了一个原生类型(raw type),它是泛型类型的名字,而没有任何相应的参数类型[JLS, 4.8]。例如,相对于List<E>的原生类型是List。原生类型的行为就像所有的泛型类型信息从类型声明中擦除。它们主要是为了和以前的泛型代码相兼容而存在。

在泛型加入到Java之前,以下是一个典型的集合声明。在Java9中,它仍旧是合法的,但是远非可仿效的:

// 原生类型 - 不要这么做!

// 我的邮票集合。仅仅包含Stamp实例。 
private final Collection stamps = ... ;

如果你今天使用这个声明,而且意外地把硬币放入到你的邮票集合中,那么这个错误的插入正常编译,而且运行没有错误(尽管编译器发出一个模糊警告):

// 硬币错误插入到邮票集合 
stamps.add(new Coin( ... )); // 发出“非受检查的调用”的警告

你不会遇见一个错误,直到你试着从邮票集合中获取这个硬币:

// 原生迭代器类型 - 不要这么用!
for (Iterator i = stamps.iterator(); i.hasNext(); )
    Stamp stamp = (Stamp) i.next(); // 抛出ClassCastException 
        stamp.cancel();

就像这本书自始至终提到的,在构建它们之后,最好在编译的时候,尽早发现错误是值得的。这这个情况中,你直到运行时才会发现错误,远在它发生之后,而且是在远离于包含这个错误的代码的代码中。一旦你看见了ClassCastException,你不得不搜索代码库,寻找把这个硬币放入到邮票集合的方法调用。编译器不会帮你,因为它不能理解注释说“仅仅包含Stamp实例”。

使用泛型,类型声明包含这个信息,而不是注释:

// 参数化的结合类型 - 类型安全的 
private final Collection<Stamp> stamps = ... ; 

从这个声明中,编译器知道stamps应该仅仅包含Stamp实例,而且保证它是正确的,而且你的整个代码库正常编译,没有发出(或者抑制;参考条目27)任何警告。当stamps以参数化类型声明方式声明时,错误插入产生一个编译时错误信息,精确地告诉你是什么错误:

Test.java:9: error: incompatible types: Coin cannot be converted to Stamp
c.add(new Coin()); 
          ^ 

当你从集合中获取元素时,编译器为你插入了不可见的强转,而且保证了它们不会失败(再次,你的所有代码不会产生或者抑制任何编译警告)。虽然不慎把硬币插入到一个邮票集合的情况可能显得牵强附会的,但是这个问题是实际的。例如,容易想象,把BigInteger放到一个集合,这个集合被认为是只包含BigDecimal实例。

就像前面提到的,使用原生类型(没有类型参数的泛型类型)是合法的,但是你不应该这么做。如果你使用原生类型,那么是失去了泛型的所有优点:安全类型和表达性。既然你不应该使用它们,那么为什么语言设计者起初允许原生类型呢?为了兼容性。当泛型加入时,Java快进入了它的第二个十年,有未使用泛型的大量代码已经存在。这些代码仍旧是合法的,而且和使用泛型的更新代码可以互操作,这注定是至关重要的。它不得不是合法的:把参数化类型的实例传递到一个方法,这个方法是设计为原生类型使用的,或者相反。这个要求,被称为迁移兼容性(migration compatibility),使得做出了这个决定:支持原生类型和使用擦除(erasure)实现泛型(条目28)。

虽然你不应该使用原生类型,比如List,但是使用参数化为允许插入任意对象的类型,比如List<Object>,这是合适的。那么原生类型List和参数化类型List<Object>的区别是什么?大致地讲,前者不在泛型类型系统中,而后者则显式地告诉编译器,它可以留存任何类型的对象。虽然你可以把List<String>传递到类型参数List,但是你不能把它传递到类型参数列表List<Object>。泛型有子类型化的规则,List<String>是原始类型List的子类型,但不是参数化类型List<Object>的子类型(条目28)。因此,如果你使用了原生类型,比如List,那么你失去了类型安全,但是如果你使用参数化类型,比如List<Object&gt,那么你没有失去

为了使得这个具体,考虑下面的程序:

// 运行时失败 - unsafeAdd方法使用了原生类型(List)!
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // 编译器产生的强转 
}

private static void unsafeAdd(List list, Object o) { 
    list.add(o); 
} 

这个程序可以编译,但是因为它使用了原生类型,你获得一个警告:

Test.java:10: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
list.add(o); 
        ^

事实上,如果你运行这个程序,那么当程序试着把strings.get(0)调用的结果(它是一个Integer)强转为一个String,你得到一个ClassCastException。这是编译器产生的强转,所以他通常保证了成功,但是在这种情况下,我们忽略了一个编译器警告而且付出了代价。

如果你使用unsafeAdd声明中参数化类型List<Object>代替原生类型,而且尝试重新编译这个程序,那么你将发现它不再能编译而且抛出错误信息:

Test.java:5: error: incompatible types: List<String> cannot be converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42)); 
    ^

你可能想着为一个集合使用原生类型,这个集合的元素类型是未知的而且也不重要。例如,假设你想编写一个接受了两个集的方法,而且返回了它们相同元素的个数。如果你对泛型不熟悉,以下是你可能这么编写这样的方法:

// 使用未知元素类型的原生类型 - 不要这么做!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1)) 
            result++;
    return result; 
}

这个方法起作用,但是它使用了原生类型,这是危险的。安全的替代方案是,使用非受限通配符(unbounded wildcard)类型。如果你想使用一个泛型类型,但是你不知道也不关心实际类型参数是什么,那么你可以用问号替代。例如,泛型类型Set<E>的非受限通配符类型是 Set<?>(读做“某个类型的集”)。这个是最通用的参数化Set类型,它可以保留任何集。以下是使用非受限通配符类型的numElementsInCommon声明看上去的样子:

// 使用无上限通配符类型 - 类型安全的和灵活的
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

非受限通配符Set<?>和原生类型Set的区别是什么呢?问号真得就适合任何情况?不要过度阐述这个点,但是通配符类型是安全的而原生类型不是。你可以把任何元素放入到一个原生类型的集合,轻易地破坏了集合的类型不变性(就像119页的unsafeAdd方法所展示的);你不要把任何元素(除了null)放入到Collection<?>中。尝试这么做将会产生编译时如下的错误信息:

WildCard.java:13: error: incompatible types: String cannot be converted to CAP#1
c.add("verboten"); 
    ^ 
    where CAP#1 is a fresh type-variable:
      CAP#1 extends Object from capture of ?

无可否认,这个错误信息让一些事情有待改进,但是编译器完成了它的工作,防止你破坏集合的类型不变性,不管它的元素类型是什么。不仅你不能把任何元素(除了null)放入到Collection<?>中,而且你不能假设关于你获得对象的类型的任何事情。如果这些限制是不能接受的,那么你应该使用泛型方法(generic method)(条目30),或者受限通配(bounded wildcard)类型(条目31)。

对于你不应该使用原生类型这个规则,有一些小小的例外。你必须在类字面常量中使用原生类型。这个规范不允许使用参数化类型(尽管他不允许array类型和原始类型)[JLS, 15.8.2]。换句话说,List.class、String[].class和int.class全是合法的,但是List<String>.class和List<?>.class不是合法的。

第二个例外是,这个规则是关于instanceof操作子。因为泛型类型信息在运行时擦除,所以在参数化类型上使用instanceof操作子是不合法的,而不是非受限通配符类型。非受限通配符类型,代替原生类型的使用时,在任何情况下都不会影响instanceof操作子的行为。这种情况下,尖括号和问号只是噪音。以下是使用用泛型类型的instanceof操作子的更可取的方法

// 原生类型的合法使用 - instanceof操作子
if (o instanceof Set) { // 原生类型
    Set<?> s = (Set<?>) o; // 通配符类型
    ... 
} 

注意到,一旦你决定o是一个Set,那么你必须把它强转到通配符类型Set<?>,而不是原生类型Set。这是一个受检查的强转,所以它不会造成一个编译器警告。

总之,使用原生类型可能导致运行时异常,所以不要使用它们。它们仅仅是为了兼容性和遗留代码的互操作而存在,这个遗留代码早于泛型的引入。快速回顾下,Set<Object>是参数化类型,代表一个可以包含任何类型的对象;Set<?>是通配符类型,代表仅仅包含某个未知类型的对象;Set是一个原生类型,它从泛型类型系统退出了。前两个是安全的,最后一个则不是。

作为一个快速参考,这个条目中(一些由这章后续引入)引入的术语总结在如下表格中:

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

推荐阅读更多精彩内容

  • 声明中具有一个或者多个类型参数的类或者接口,就是泛型类或者接口。没有使用泛型的类型就是原生态类型。 每种泛型定义一...
    慧执行阅读 575评论 0 0
  • 无事容易生非,这话被我今天淋漓尽致地演绎了一番。 今早没有早读,课也是下午的,孩子起床走了后,我准备再睡一会,却翻...
    暂且这样吧阅读 222评论 0 0
  • 不得不承认自己是个很天真的人,谈了恋爱就想过一辈子,交了朋友就想往来一生。尽管有时候故作姿态说着一切顺其自然,可心...
    金夏阅读 439评论 0 1
  • 如果说默契是检验爱情在不在的唯一标准,那么,床,是检验性爱在不在的唯一标准。 门被推开的刹那,我和老公刘旭两人,哦...
    小玩具妈妈阅读 3,570评论 116 107
  • 早上送完二个小的上幼儿园,去203看到窗户紧团,打电话给房东,不在那,要星期日过来,而且他说你没拿走的东西都清理出...
    向往精灵阅读 361评论 0 0