Tips

《Effective Java, Third Edition》一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将近8年的时间,但随着Java 6,7,8,甚至9的发布,Java语言发生了深刻的变化。

在这里第一时间翻译成中文版。供大家学习分享之用。

自Java 5以来,泛型已经成为该语言的一部分。 在泛型之前,你必须转换从集合中读取的每个对象。 如果有人不小心插入了错误类型的对象,则在运行时可能会失败。 使用泛型,你告诉编译器在每个集合中允许哪些类型的对象。 编译器会自动插入强制转换,并在编译时告诉你是否尝试插入错误类型的对象。 这样做的结果是既安全又清晰的程序,但这些益处,不限于集合,是有代价的。 本章告诉你如何最大限度地提高益处,并将并发症降至最低。

26. 不要使用原始类型

首先,有几个术语。一个类或接口,它的声明有一个或多个类型参数( type parameters ),被称之为泛型类或泛型接口[JLS,8.1.2,9.1.2]。 例如,List接口具有单个类型参数E,表示其元素类型。 接口的全名是List<E>(读作“E”的列表),但是人们经常称它为List。 泛型类和接口统称为泛型类型(generic types)。

每个泛型定义了一组参数化类型(parameterized types),它们由类或接口名称组成,后跟一个与泛型类型的形式类型参数[JLS,4.4,4.5]相对应的实际类型参数的尖括号“<>”列表。 例如,List<String>(读作“字符串列表”)是一个参数化类型,表示其元素类型为String的列表。 (String是与形式类型参数E相对应的实际类型参数)。

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

在泛型被添加到Java之前,这是一个典型的集合声明。 从Java 9开始,它仍然是合法的,但并不是典型的声明方式了:

// Raw collection type - don't do this!

// My stamp collection. Contains only Stamp instances.
private final Collection stamps = ... ;

如果你今天使用这个声明,然后不小心把coin实例放入你的stamp集合中,错误的插入编译和运行没有错误(尽管编译器发出一个模糊的警告):

// Erroneous insertion of coin into stamp collection
stamps.add(new Coin( ... )); // Emits "unchecked call" warning

直到您尝试从stamp集合中检索coin实例时才会发生错误:

// Raw iterator type - don't do this!
for (Iterator i = stamps.iterator(); i.hasNext(); )
Stamp stamp = (Stamp) i.next(); // Throws ClassCastException
stamp.cancel();

正如本书所提到的,在编译完成之后尽快发现错误是值得的,理想情况是在编译时。 在这种情况下,直到运行时才发现错误,在错误发生后的很长一段时间,以及可能远离包含错误的代码的代码中。 一旦看到ClassCastException,就必须搜索代码类库,查找将coin实例放入stamp集合的方法调用。 编译器不能帮助你,因为它不能理解那个说“仅包含stamp实例”的注释。

对于泛型,类型声明包含的信息,而不是注释:

// Parameterized collection type - typesafe
private final Collection<Stamp> stamps = ... ;

从这个声明中,编译器知道stamps集合应该只包含Stamp实例,并保证它是true,假设你的整个代码类库编译时不发出(或者抑制;参见条目27)任何警告。 当使用参数化类型声明声明stamps时,错误的插入会生成一个编译时错误消息,告诉你到底发生了什么错误:

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

当从集合中检索元素时,编译器会为你插入不可见的强制转换,并保证它们不会失败(再假设你的所有代码都不会生成或禁止任何编译器警告)。 虽然意外地将coin实例插入stamp集合的预期可能看起来很牵强,但这个问题是真实的。 例如,很容易想象将BigInteger放入一个只包含BigDecimal实例的集合中。

如前所述,使用原始类型(没有类型参数的泛型)是合法的,但是你不应该这样做。 如果你使用原始类型,则会丧失泛型的所有安全性和表达上的优势。 鉴于你不应该使用它们,为什么语言设计者首先允许原始类型呢? 答案是为了兼容性。 泛型被添加时,Java即将进入第二个十年,并且有大量的代码没有使用泛型。 所有这些代码都是合法的,并且与使用泛型的新代码进行交互操作被认为是至关重要的。 将参数化类型的实例传递给为原始类型设计的方法必须是合法的,反之亦然。 这个需求,被称为迁移兼容性,驱使决策支持原始类型,并使用擦除来实现泛型(条目 28)。

虽然不应使用诸如List之类的原始类型,但可以使用参数化类型来允许插入任意对象(如List<Object>)。 原始类型List和参数化类型List<Object>之间有什么区别? 松散地说,前者已经选择了泛型类型系统,而后者明确地告诉编译器,它能够保存任何类型的对象。 虽然可以将List<String>传递给List类型的参数,但不能将其传递给List<Object>类型的参数。 泛型有子类型的规则,List<String>是原始类型List的子类型,但不是参数化类型List<Object>的子类型(条目 28)。 因此,如果使用诸如List之类的原始类型,则会丢失类型安全性,但是如果使用参数化类型(例如List <Object>)则不会。

为了具体说明,请考虑以下程序:

// 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)的结果(一个Integer)转换为一个String时,会得到ClassCastException异常。 这是一个编译器生成的强制转换,因此通常会保证成功,但在这种情况下,我们忽略了编译器警告并付出了代价。

如果用unsafeAdd声明中的参数化类型List <Object>替换原始类型List,并尝试重新编译该程序,则会发现它不再编译,而是发出错误消息:

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

你可能会试图使用原始类型来处理元素类型未知且无关紧要的集合。 例如,假设你想编写一个方法,它需要两个集合并返回它们共同拥有的元素的数量。 如果是泛型新手,那么您可以这样写:

// 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;
}

这种方法可以工作,但它使用原始类型,这是危险的。 安全替代方式是使用无限制通配符类型(unbounded wildcard types)。 如果要使用泛型类型,但不知道或关心实际类型参数是什么,则可以使用问号来代替。 例如,泛型类型Set<E>的无限制通配符类型是Set <?>(读取“某种类型的集合”)。 它是最通用的参数化的Set类型,能够保持任何集合。 下面是numElementsInCommon方法使用无限制通配符类型声明的情况:

// Uses unbounded wildcard type - typesafe and flexible
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 <?>中,但是不能保证你所得到的对象的类型。 如果这些限制是不可接受的,可以使用泛型方法(条目 30)或有限制配符类型(条目 31)。

对于不应该使用原始类型的规则,有一些小例外。 你必须在类字面值(class literals)中使用原始类型。 规范中不允许使用参数化类型(尽管它允许数组类型和基本类型)[JLS,15.8.2]。 换句话说,List.classString [] .classint.class都是合法的,但List <String> .classList <?>.class不是合法的。

规则的第二个例外涉及instanceof操作符。 因为泛型类型信息在运行时被删除,所以在无限制通配符类型以外的参数化类型上使用instanceof运算符是非法的。 使用无限制通配符类型代替原始类型不会以任何方式影响instanceof运算符的行为。 在这种情况下,尖括号和问号就显得多余。 以下是使用泛型类型的instanceof运算符的首选方法:

// Legitimate use of raw type - instanceof operator
if (o instanceof Set) { // Raw type
Set<?> s = (Set<?>) o; // Wildcard type
...
}

请注意,一旦确定o对象是一个Set,则必须将其转换为通配符Set <?>,而不是原始类型Set。 这是一个强制转换,所以不会导致编译器警告。

总之,使用原始类型可能导致运行时异常,所以不要使用它们。 它们仅用于与泛型引入之前的传统代码的兼容性和互操作性。 作为一个快速回顾,Set<Object>是一个参数化类型,表示一个可以包含任何类型对象的集合,Set<?>是一个通配符类型,表示一个只能包含某些未知类型对象的集合,Set是一个原始类型,它不在泛型类型系统之列。 前两个类型是安全的,最后一个不是。

为了快速参考,下表中总结了本条目(以及本章稍后介绍的一些)中介绍的术语:

术语 中文含义 举例 所在条目
Parameterized type 参数化类型 List<String> 条目 26
Actual type parameter 实际类型参数 String 条目 26
Generic type 泛型类型 List<E> 条目 26
Formal type parameter 形式类型参数 E 条目 26
Unbounded wildcard type 无限制通配符类型 List<?> 条目 26
Raw type 原始类型 List 条目 26
Bounded type parameter 限制类型参数 <E extends Number> 条目 29
Recursive type bound 递归类型限制 <T extends Comparable<T>> 条目 30
Bounded wildcard type 限制通配符类型 List<? extends Number> 条目 31
Generic method 泛型方法 static <E> List<E> asList(E[] a) 条目 30
Type token 类型令牌 String.class 条目 33

Effective Java 第三版——26. 不要使用原始类型的更多相关文章

  1. 《Effective Java 第三版》新条目介绍

    版权声明:本文为博主原创文章,可以随意转载,不过请加上原文链接. https://blog.csdn.net/u014717036/article/details/80588806前言 从去年的3月份 ...

  2. 《Effective Java 第三版》目录汇总

    经过反复不断的拖延和坚持,所有条目已经翻译完成,供大家分享学习.时间有限,个别地方翻译得比较仓促,希望有疑虑的地方指出批评改正. 第一章简介 忽略 第二章 创建和销毁对象 1. 考虑使用静态工厂方法替 ...

  3. Effective Java 第三版——30. 优先使用泛型方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  4. Effective Java 第三版——34. 使用枚举类型替代整型常量

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  5. Effective Java 第三版——1. 考虑使用静态工厂方法替代构造方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. Effective Java 第三版——3. 使用私有构造方法或枚类实现Singleton属性

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  7. Effective Java 第三版——7. 消除过期的对象引用

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  8. Effective Java 第三版——9. 使用try-with-resources语句替代try-finally语句

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  9. Effective Java 第三版——10. 重写equals方法时遵守通用约定

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

随机推荐

  1. [转]Android sharedpreferences使用

    1.    SharedPerferences保存的数据主要是类似配置信息格式的数据,因此它保存的数据主要是简单类型的key-value对,SharedPreferences本身并没有写入数据的能力, ...

  2. 一、 kettle开发、上线常见问题以及防错规范步骤

    此篇说明对应的kettle版本是6.1,实际使用时7.x应该也是一样的. 一.    kettle开发流程(规范步骤,防止出错) (一)       Kettle设置检查 资源库连接 如果不加一下配置 ...

  3. TensorFlow常用的函数

    TensorFlow中维护的集合列表 在一个计算图中,可以通过集合(collection)来管理不同类别的资源.比如通过 tf.add_to_collection 函数可以将资源加入一个 或多个集合中 ...

  4. Javascript流程控制

    Javascript流程控制 1.条件语句 (1)if(exp)执行一句代码 (2)if(exp){执行代码段;} (3)if(exp){exp为true执行代码段}else{exp为false执行的 ...

  5. python pandas 合并数据函数merge join concat combine_first 区分

    pandas对象中的数据可以通过一些内置的方法进行合并:pandas.merge,pandas.concat,实例方法join,combine_first,它们的使用对象和效果都是不同的,下面进行区分 ...

  6. element-ui+vuex共享自定义方法进行表单验证 validator

    element-ui的官网上写的自定义表单验证,方法都是写在单vue文件中的,不容易共享.怎么使用vuex将方法共享出来,各个组件都能用呢? 如下是一个验证age的数据, rules:{ age:[{ ...

  7. CommonJS, AMD ,CMD之间的关系

    commonjs是用在服务器端的,同步的,如nodejs amd, cmd是用在浏览器端的,异步的,如requirejs和seajs 其中,amd先提出,cmd是根据commonjs和amd基础上提出 ...

  8. 解决 iPhone 微信 H5 无法自动播放音乐问题

    使用 wx sdk 在html导入sdk js文件.(如果网站是https导入的sdk也要使用https!) 然后在 wx.ready 中设置 play 方法并复制给全局变量 fm,当需要使用的时候再 ...

  9. Spring Cloud Consul入门

    1. Consul介绍 Consul是一套开源的分布式服务发现和配置管理系统,支持多数据中心分布式高可用.Consul是HashiCorp( Vagrant的创建者)开发的一个服务发现与配置项目,用G ...

  10. eml企业通讯录管理系统v5.0 存在sql注入

    0x00 前言 上周五的时候想练练手,随便找了个系统下载下来看看. 然后发现还有VIP版本,但是VIP要钱,看了一下演示站,貌似也没有什么改变,多了个导入功能?没细看. 搜了一下发现这个系统,压根就没 ...