Effective Java(3rd)-Item26 不要使用原始类型

  首先,有一些条款。声明具有一个或多个类型参数的类或接口是泛型类或接口 [JLS, 8.1.2, 9.1.2].。例如,接口List就有单独的类型参数,E,代表了它的元素类型。该接口的完整名字叫做List(读作“list of E”),但是人们通常简读为List。泛型类和接口统称为泛型类型
  每个泛型类型定义一组参数化类型,包括类或接口名称,后面是与泛型类型的形式类型参数相对应的实际类型参数的角度括号内的列表[JLS, 4.4,4.5].例如,List(读作“list of string”)是一个参数化类型,它表示了一个列表包含的所有元素的类型都是String(String就是形参E相对应的实际类型参数)。
  最后,每个泛型类型都定义了一个原生类型,它是在没有任何伴随类型参数的情况下使用的泛型类型的名称 [JLS, 4.8]。例如,List对应的原生类型就是List。原生类型的行为就好像所有泛型类型信息都被类型声明给擦除了。它们的存在主要是为了与预泛型代码兼容。
  在Java中加入泛型之前,这将是一个示例性的集合声明。在Java9中,它仍然是合法的,但是远非典范:

Effective Java(3rd)-Item26 不要使用原始类型_第1张图片
image.png

  如果你在今天使用这个声明,然后意外地在你的集合中加入coin,错误的插入将编译并允许而不会出错(尽管编译器确实发出了模糊的警告):


image.png

  你不会得到错误直到你尝试在stamp集合中检索coin:


image.png

  正如本书所提到的,在错误发生后尽快发现错误是有益的,最好是在编译时。在这种情况下,你将不会发现错误直到运行时,在错误发生很久之后,并且可能与包含错误的代码相距较远的代码中发现错误。一旦你看到ClassCastException,你不得不搜索代码库寻找将coin放入stamp集合的方法引用。编译器不能帮助你,因为它不能理解注释的描述:“只能包含Stamp实例”。
  使用泛型,类型声明包含信息,而不是注释:


image.png

  通过这个声明,编译器知道了stamp应该有且仅包含Stamp实例并保证这是正确的,假设整个代码库编译而不会发生任何警告(或抑制;见 item27 )。当stamps被参数化类型声明而声明,错误地插入将会产生编译时错误消息,并确切地告诉你哪里错了:

Effective Java(3rd)-Item26 不要使用原始类型_第2张图片
image.png

  编译器从集合中检索元素并保证为你插入不可见的强制类型转换时不会失败。(假设你的所有代码没有审核过程或抑制任何编译警告)。虽然意外地将coin插入stamp集合的想法可能是有点牵强,但是这个问题是存在的。·比如,很容易想象将BigInteger放入只包含BigDecimal实例的集合。
  如前所述,使用原生类型是合法的(没有类型参数的泛型类型),但是你不该这么做。如果你使用原生类型,你就失去了所有的安全性和泛型表达的好处。既然你不该去使用它们,为什么语言设计者一开始就允许使用原生类型?为了兼容。Java即将进入它的第二个十年,在这个时候加入泛型,已经存在了大量不使用泛型的代码。至关重要的是,所有这些代码都应该保持合法,并可以与使用泛型的更新的代码可以互相操作。将参数化类型的实例传递给设计与用于原始类型的方法必须是合法的,反之亦然。这一需求,即迁移兼容性,推动了支持原生类型以及使用类型擦除实现泛型的决策(item28 )。

  虽然你不该使用原生类型比如List,但是可以使用参数化的类型以允许插入任意对象,比如List。那么,原始类型List和参数化类型List之间有什么区别呢?松散地说,前者选择退出泛型类型系统,后者明确告知编译器它能够保存任何类型的对象。你可以将List传递给List,但是你不能传递给List.泛型还有子类类型规则,List是原生类型List的子类型,但是不是参数化类型List的子类型 (item28) 。因此,如果你使用原生类型比如List,你将会失去类型安全,但是如果使用参数化类型比如List,那就不会这样。

  具体一些,考虑如下的程序:


Effective Java(3rd)-Item26 不要使用原始类型_第3张图片
image.png

  这个程序编译通过了,但是因为它使用了原生类似List,你得到了一个警告:


image.png

  

事实上,如果你运行这个程序,当程序尝试强制Integer类型转换String调用Strings.get(0)的结果时,你将得到ClassCastException,因此,它通常是成功的,但是在这种情况下,我们忽略了编译器的警告,并为此付出了代价。
  如果你在unsafeAdd声明中将原生类型List替代为参数化类型List,并尝试重新编译程序,你将会得到不再编译通过,并发出错误消息:


image.png

  你可能会倾向对元素类型未知且无关紧要的集合使用原生类型。比如,假设你想要编写一个方法,该方法接受两个set,并返回它们共有的元素数。如果你对泛型还不够熟悉,下面是如何编写这样的方法:


Effective Java(3rd)-Item26 不要使用原始类型_第4张图片
image.png

  这个方法能正常工作,但是它使用了原生类型,是危险的。安全的替代应该是使用无界通配符类型。如果你想要使用泛型类型但是你不知道或者不关心实际类型是什么,你可以使用“问号”来代替。比如,泛型类型Set的无界通配符类型是Set(读作“set of some type”)。它是最通用的参数化Set类型,能容纳任何set。 如下是如何使用无界通配符类型来实现numElementsInCommon:

image.png

  无界通配符类型Set与原生类型Set这两者有什么区别呢?问号真的给了你什么吗?不要在意细节,但是通配符类型是安全的,而原始类型不是。使用原生类型可以在集合中放置任意元素,很容易破坏集合的类型不变量(第119页所示的unsafeAdd方法所示);你不能将任意元素(null除外)放到Collection 里面.尝试这么做将生成编译时间错误信息:

Effective Java(3rd)-Item26 不要使用原始类型_第5张图片
image.png

  诚然,这条错误消息不尽如人意,但是编译器已经完成了它的工作,阻止你破坏集合的类型不变量(不管它元素的类型是什么)。你不仅不能在Collection放入任意元素,同时也不能假设你得到的对象的类型。如果这些限制是不可接受的,你可以使用泛型方法 (item30) 或无界通配符类型 i(tem31)。

  对于不该使用原生类型的规则,有几个小例外。你必须在类字面常量中使用原生类型。该规范明确说明不允许使用参数化类型(虽然它确实允许数组类型和基本类型)[JLS, 15.8.2]。换句话说,List.class,String[].class ,以及int.class 都是合法的,但是List.class, 以及List.class是不合法的。
  第二个规则的特例是instanceof操作符。因为泛型类型信息在运行时被擦除了,使用instanceof操作符就是非法的,而不是无界通配符类型。使用无界通配符类型代替原生类型不会以任何方式影响instanceof运算符的行为。在这种情况下,尖括号和问号就只是噪音。**如下是使用instanceof操作符和泛型类型更好的方式

Effective Java(3rd)-Item26 不要使用原始类型_第6张图片
image.png

  注意,一旦确定o是Set,你必须强制转换为通配符类型Set,而不是原生类型Set。这是一个检查的强制转换,所以它不会造成编译警告。
  总结,使用原生类型会在运行时导致异常,所以你不要去使用它们。它们的存在甚至是为了与早于泛型引入之前的遗留代码的兼容性和互操作性。作为一个快速的回顾,Set是一个参数化类型代表了一个set可以包含任何类型的对象,Set是无界通配符类型代表了一个set只可以包含一种未知类型的对象,Set是原生类型,不属于泛型类型系统以内。前两者是安全的,但是后者不是。
  作为快速介绍,本项目介绍的输术语(以及本章后面介绍的几个术语)摘要见下表:


Effective Java(3rd)-Item26 不要使用原始类型_第7张图片
image.png

本文写于2019.6.1,历时7天

你可能感兴趣的:(Effective Java(3rd)-Item26 不要使用原始类型)