Effective-java 3 中文翻译系列 (Item 26 不要使用原始类型)

原文链接

文章也上传到

github

(欢迎关注,欢迎大神提点。)


ITEM26 不要使用原始类型

从Java5开始引入范型。
在没有范型的时候,如果有人不小心将错误的类型加入到collection中,就会造成运行时的错误。
有了范型之后,你能告诉编译器,哪种类型被允许加入到collection 中,而且能在编译期间就发现错误。这个系列的文章会告诉你如何更优雅的使用范型。

首先,有几个术语解释一下。一个类或者接口如果声明了一个或多个类型参数,就可以被叫做范型类或范型接口。例如,List接口有一个类型参数:E,代表元素的类型,接口的全名是:List<E>.读作E列表。但是大多数人简单读作列表。范型类和范型接口被统称为范型类型。
每个范型类型的组成是在类名或者接口名后面跟着一对尖括号,里面是真实参数类型对应范型类型的列表。例如List<String>(读作string列表)代表的是每一个元素都是String类型的list(String是对应形参E的真实参数类型)。
每一个范型都定义了一个原始类型,如List<E>中的List(它们的存在主要是为了兼容范型之前的代码)。


// 邮票集合. 只能存储邮票实例.
private final Collection stamps = ... ;

如果你这样声明而且错误的放了一个不是邮票的实例进入到这个集合中,编译和运行期间并不会报错:

//错误的放一个coin类型实例进入邮票集合中
stamps.add(new Coin( ... )); // Emits "unchecked call" warning

直到你尝试从集合中取出这个coin时才会报错:

for (Iterator i = stamps.iterator(); i.hasNext();)
    Stamp stamp = (Stamp) i.next(); // 抛出异常ClassCastException
stamp.cancel();

但是我们的原则是越早发现问题越好,最好是在编译期就能发现问题。在上面这种情况下,在运行时也只能到执行到上面代码时才发生错误。而且编译器不会告诉你是因为添加了coin进入stamps导致的,它仅能告诉你“Contains only Stamp instances.”。

如果使用范型的话,可以指定类型:

private final Collection<Stamp> stamps = ... ;

编译期就知道stamps只能包含stamp类型的实例并且保证这个规则是被满足的。如果插入其他类型的实例(比如coin),编译器会告诉你发生了错误:

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

编译器会隐含的添加强转的逻辑,并保证它们不会失败。把coin加入stamp集合的例子虽然看起来不太恰当,但这也确实会发生:例如很容易会把BitInteger对象放到BitDecimal集合中。

即使使用原始类型是合法的,但是你也尽量不要这样做。因为你是使用原始类型会失去使用范型的安全性和表达的便利性。 既然你不应该使用它们,那么为什么语言还要设计允许你使用呢?答案是为了兼容性。java出现范型的时候是它被发明出来的十年后,当时已经存在大量的代码没有使用范型,所以老的代码应该是合法的,并且老代码也应该是可以和范型正常交互的,方法可以正常的传递真实类型参数,反之亦然。这被叫做迁移兼容性

即使你不使用像List一样的具体真实类型,你也可以很方便的使用参数化类型来允许插入任何实例类型,像List<Object>。那么原始类型List和List<Object>有什么不同呢?简单来说,后者明确的告诉编译器它能添加对象类型的参数。你能将List<String>传递到List,但你不能将其传递到List<Object>。范型是有子类型规则的,List<String>是List的子类型,但是不是List<Object>的子类(Item28)。总结,使用像List的原始类型,就会失去类型安全性。具体说明请看例子:

// Fails at runtime - unsafeAdd method uses a raw type
(List)!
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // Has compiler-generated cast
}

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)时,你会得到ClassCastException异常。因为要将Integer转换成String报错。如果你把List替换成List<Object>然后重新编译程序,你将会发现不能编译通过而是发生错误。

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

你可能想使用原始类型作为集合,包含一些未知类型的对象。例如,假如你想写一个方法返回两个sets中所共有的元素个数,代码如下:


// Use of raw type for unknown element type - don't do
this!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
        
    return result;
}   

这样的方法可以正常工作,但是是危险的。更安全的方法是使用无限制通配符类型
如果当你想使用范型,但是你又不知道或者不确定真实类型是什么,你可以使用一个问号标志代替。例如,Set<E>的无限制通配符号类型是Set<?>,读作某种类型的set。这是一种可用范围更广的Set类型,可以包含任何set。将numElementsInCommon方法声明称无限制通配符类型是:

// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

Set<?>和Set类型的区别是什么呢?
通配符是更加安全的。因为你可以将任何原始类型元素放入到一个集合中,这样会很容易的破坏集合类型的不变性(就像上面的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<?>,而且不能确定你从集合中取出的是何种类型。如果这些限制对你来说不可接受,那么你就可以使用范型方法(Item30)或者无限制的通配符号类型(Item31)。

但是对于这条限制也有一些特殊的例外情况你必须使用原始类型。

  • 字面值类型
  • 使用instanceof
if (o instanceof Set) { // Raw type
    Set<?> s = (Set<?>) o; // Wildcard type
    ...
}

快速回顾一下:

  • Set<Object>代表可以存放任何对象类型的set;
  • Set<?>表示可以存放任何未知对象类型的set;
  • Set是原始类型,根据范型系统输出。
    前两个是安全的,最后一个不是。

最后介绍下术语:

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

推荐阅读更多精彩内容