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. python3 python2 import 的区别

    https://stackoverflow.com/questions/12172791/changes-in-import-statement-python3

  2. 在Maven Central发布中文API的Java库

    原址: https://zhuanlan.zhihu.com/p/28024364 相关问题: 哪些Java库有中文命名的API? 且记下随想. 之前没有发布过, 看了SO上的推荐:Publish a ...

  3. 洛谷 P1485 火枪打怪

    题目描述 LXL进入到了一片丛林,结果他发现有n只怪物排成一排站在他面前.LXL有一杆火枪能对付这些怪物.他知道从左至右数第i只怪物的血量是mi.现在LXL可以将一些子弹射向某个怪物.LXL可以控制他 ...

  4. python键盘读入的input方法

    今天了解了一下python,学了一些小东西,便存下了: input函数(raw_input在3.0版本没有,所以就不说了!) num = input ("输入一个数:")  |备注 ...

  5. 老版VC++线程池

    在一般的设计中,当需要一个线程时,就创建一个,但是当线程过多时可能会影响系统的整体效率,这个性能的下降主要体现在:当线程过多时在线程间来回切换需要花费时间,而频繁的创建和销毁线程也需要花费额外的机器指 ...

  6. C和Lua之间的相互调用

    前面的话 第一次接触Lua是因为Unity游戏中需要热更,但是一直没搞懂Lua是怎么嵌入到别的语言中执行的,如何互相调用的.这次打算好好了解一下C跟lua是如何交互的 那么如何使用Lua语言? lua ...

  7. 多工程联编,cocopods的使用

    最近在使用coco2d-x, 想要使用 cocopods,发现我的目录结构如下图 发现我有三个工程文件,  那么 就需要多工程联编使用cocopods, workspace 'xcworkspace' ...

  8. 设计模式六大原则(PHP)

    设计模式的目的是为了更好的代码重用性,可读性,可靠性和可维护性.常用的六大设计模式有:单一职责原则(SRP),里氏替换原则(LSP),依赖倒转原则(DIP),接口隔离原则(ISP),迪米特法则(LOD ...

  9. 前端学习:html基础学习二

    3.文档设置标记上-格式标记(主要内容标记<br><p><center><pre><li><ul><ol><d ...

  10. AWS上获取监控数据(EC2/RDS都支持)

    方法1:mon-cmd http://docs.aws.amazon.com/zh_cn/AmazonCloudWatch/latest/cli/SetupCLI.html(安装连接) ● Step ...