通常来说,将声明参数化并使用JDK提供的泛型类型和方法并不太困难。编写你自己的泛型类型有点难,但是值得努力去学习。
考虑Item7的简单栈实现:
image.png
这个类应该一开始就被参数化,但是由于它不是参数化的,所以我们可以在事实之后对它进行泛化。换句话说,我们可以在不损害原始非参数化版本的客户端的情况下将其参数化。目前,客户端必须对堆栈中弹出的对象进行强制转换,而这些转换可能在运行时失败。生成类的第一步是将一个或多个类型参数添加到类的声明。在本例中有一个类型参数,表示堆栈的元素类型,该类型参数的常规名称是E(item68 ).
下一步是用适当的类型参数替换类型为Object的所有用法,然后尝试编译生成的程序:
image.png
通常至少会收到一个错误或经过,这个类也不例外。幸运的是,这个类只生成一个错误:
image.png
正如item28所解释的,你不能创建不可还原类型的数组,例如E。每当你编写由数组支持的泛型类型时,这个问题每次都会出现。这里有两种合理的解决方案。第一个解决方案直接绕过了对泛型数组创建的禁止:创建一个Object的数组并强制转为泛型数组类型。现在,编辑器将发出一个经过,而不是错误。这个用法是合法的,但是不是类型安全的(一般而言):
image.png
编译器可能无法证明你的程序是类型安全的,但是你可以。你必须让自己相信,未经检查的强制转换不会损害程序的类型安全性。所讨论的数组(元素)存储在私有字段中,从未返回给客户端或者传递给任何其他方法。数组中存储的唯一元素是传递给push方法的元素,他们的类型是E,因此未检查的强制转换不会造成任何损害。
一旦你已经证明了一个未经检查的强制转换是安全的,请在尽可能窄的范围内抑制警告(item27 )。在这种情况下,构造方法只包含未检查的数组创建,因此应该在整个构造方法中抑制警告。通过添加一个注解来实现这一点,Stack可以干净地编译,并且使用它不需要显式的强制转换或恐惧ClassCastException。
消除Stack中泛型数组创建错误的第二种方法是将字段元素的类型从E[]更改为Object[]。如果你这么做,你将得到一个不同的错误:
image.png
你可以通过强制转换检索的元素为E来使错误转为警告,你将得到如下警告:
image.png
因为E是不可重用的类型,所以编译器无法在运行时检查强制转换。同样,你可以很容易地向自己证明未经检查的强制转换是安全的,所以抑制警告是合适的。根据 item77的建议,我们仅对包含未检查的强制转换的声明抑制,而不是整个pop方法:
image.png
消除泛型数组创建的两种技巧都有他们的追随者。第一个更可读:数组声明为E[]类型,清楚地指示了它只包含E实例。它也更简练:在典型的泛型类中,可以在代码的许多地方从数组中读取;第一种技巧只需要一次强制转换(创建数组的位置),而第二种技巧在每次读取数组元素时都要单独的强制转换。因此,第一种技巧更可取,并且在实际的应用中更常用。当然,它的确会造成堆污染(item32): 数组的运行时类型与其编译时类型不匹配(除非E碰巧是Object)。这使得一些程序员非常不愿意选择第二种技巧,尽管在这种情况下堆污染是无害的。
下面的程序演示了我们的Stack类的用法。该程序以反向顺序打印其命令行参数,并转换为大写。在Stack中弹出的元素上调用String的toUpperCase方法不需要显式强制转换,自动生成的强制转换一定会成功:
image.png
上述例子似乎与item28相矛盾,该项目管理优先使用列表而不是数组。在泛型类型中使用列表并不总是可能是可取的。Java原生并不支持list,因此一些泛型类型,比如ArrayList,必须在数组之上实现。其他泛型类型(入HashMap)就是基于数据之上实现的,以提高性能。
大多数泛型类型与我们的Stack例子类似,因为它们的类型参数没有限制:你可以创建一个Stack<Object>,Stack<Integer[]>,Stack<List<String>>,或任何其他引用类型的对象。注意到,你不能创建一个原生类型的Stack:创建一个Stack<int>或Stack<double>将造成编译时异常。这是Java泛型系统的一个基本限制。你可以通过装箱基本类型来绕过这个限制( item61 )。
有写泛型类型会限制其类型参数的允许值。比如,考虑 java.util.concurrent.DelayQueue,它的声明长这样:
image.png
这个类型参数 list (<E extends Delayed>)需要参数E的实际类型是java.util.concurrent.Delayed的子类型。 这允许DelayQueue实现其客户端利用DelayQueue元素上的Delayed方法,而不需要显式转换而发生ClassCastException。类型参数E称为有界类型参数。请注意,定义了子类型关系,以便每个类型都是其自身的一个子类型 [JLS, 4.10],所以创建 DelayQueue<Delayed>是合法的。
总结,在客户端代码使用泛型类型比强制转换更安全更简单。当你设置一个新类型的时候,确保他们可以不需要通过强制转换而被使用。这通常意味着使类型成为泛型。如果你有任何应该是泛型但不是泛型的现有类型,请泛化他们。这将使这些类型的新用户在不破坏现有客户端的情况下更容易使用(item26)
本文写于2019.6.26,历时1天
网友评论