Java理论与实践: 利用通配符简化泛型利用
副标题#e#
自从泛型被添加到 JDK 5 语言以来,它一直都是一个颇具争议的话题。一部 分人认为泛型简化了编程,扩展了范例系统从而使编译器可以或许检讨范例安详;另 外一些人认为泛型添加了许多不须要的巨大性。对付泛型我们都经验过一些疾苦 的回想,但毫无疑问通配符是最棘手的部门。
通配符根基先容
泛型是一种暗示类或要领行为对付未知范例的范例约束的要领,好比 “不管 这个要领的参数 x 和 y 是哪种范例,它们必需是沟通的范例”,“必需为这些 要领提供同一范例的参数” 可能 “foo() 的返回值和 bar() 的参数是同一类 型的”。
通配符 — 利用一个奇怪的问号暗示范例参数 — 是一种暗示未知范例的类 型约束的要领。通配符并不包括在最初的泛型设计中(发源于 Generic Java (GJ)项目),从形成 JSR 14 到宣布其最终版本之间的五年多时间内完成设计 进程并被添加到了泛型中。
通配符在范例系统中具有重要的意义,它们为一个泛型类所指定的范例荟萃 提供了一个有用的范例范畴。对泛型类 ArrayList 而言,对付任意(引用)类 型 T,ArrayList<?> 范例是 ArrayList<T> 的超范例(雷同原始 范例 ArrayList 和根范例 Object,可是这些超范例在执行范例揣度方面不是很 有用)。
通配符范例 List<?> 与原始范例 List 和详细范例 List<Object> 都不沟通。假如说变量 x 具有 List<?> 范例,这 暗示存在一些 T 范例,个中 x 是 List<T>范例,x 具有沟通的布局,尽 管我们不知道其元素的详细范例。这并不暗示它可以具有任意内容,而是指我们 并不相识内容的范例限制是什么 — 但我们知道存在 某种限制。另一方面,原 始范例 List 是异构的,我们不能对其元素有任何范例限制,详细范例 List<Object> 暗示我们明晰地知道它能包括任何工具(虽然,泛型的类 型系统没有 “列表内容” 的观念,但可以从 List 之类的荟萃范例轻松地领略 泛型)。
通配符在范例系统中的浸染部门来自其不会产生协变(covariant)这一特性 。数组是协变的,因为 Integer 是 Number 的子范例,数组范例 Integer[] 是 Number[] 的子范例,因此在任何需要 Number[] 值的处所都可以提供一个 Integer[] 值。另一方面,泛型不是协变的, List<Integer> 不是 List<Number> 的子范例,试图在要求 List<Number> 的位置提供 List<Integer> 是一个范例错误。这不算很严重的问题 — 也不是所有人 都认为的错误 — 但泛型和数组的差异行为简直引起了很多杂乱。
我已利用了一个通配符 — 接下来呢?
清单 1 展示了一个简朴的容器(container)范例 Box,它支持 put 和 get 操纵。 Box 由范例参数 T 参数化,该参数暗示 Box 内容的范例, Box<String> 只能包括 String 范例的元素。
清单 1. 简朴的泛型 Box 范例
public interface Box<T> {
public T get();
public void put(T element);
}
通配符的一个长处是答允编写可以操纵泛型范例变量的代码,而且不需要了 解其详细范例。譬喻,假设有一个 Box<?> 范例的变量,好比清单 2 unbox() 要领中的 box 参数。unbox() 如那里理惩罚已通报的 box?
清单 2. 带有通配符参数的 Unbox 要领
public void unbox(Box<?> box) {
System.out.println(box.get());
}
事实证明 Unbox 要领能做很多事情:它能挪用 get() 要领,而且能挪用任 何从 Object 担任而来的要领(好比 hashCode())。它惟一不能做的事是挪用 put() 要领,这是因为在不知道该 Box 实例的范例参数 T 的环境下它不能检讨 这个操纵的安详性。由于 box 是一个 Box<?> 而不是一个原始的 Box, 编译器知道存在一些 T 充当 box 的范例参数,但由于不知道 T 详细是什么, 您不能挪用 put() 因为不能检讨这么做不会违反 Box 的范例安详限制(实际上 ,您可以在一个非凡的环境下挪用 put():当您通报 null 字母时。我们大概不 知道 T 范例代表什么,但我们知道 null 字母对任何引用范例而言是一个空值 )。
关于 box.get() 的返回范例,unbox() 相识哪些内容呢?它知道 box.get() 是某些未知 T 的 T,因此它可以揣度出 get() 的返回范例是 T 的擦除 (erasure),对付一个无上限的通配符就是 Object。因此清单 2 中的表达式 box.get() 具有 Object 范例。
#p#副标题#e#
通配符捕捉
清单 3 展示了一些好像应该 可以事情的代码,但实际上不能。它包括一个 泛型 Box、提取它的值并试图将值放回同一个 Box。
清单 3. 一旦将值从 box 中取出,则不能将其放回
#p#分页标题#e#
public void rebox(Box<?> box) {
box.put(box.get());
}
Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied
to (java.lang.Object)
box.put(box.get());
^
1 error
这个代码看起来应该可以事情,因为取出值的范例切合放回值的范例,然而 ,编译器生成(令人狐疑的)关于 “capture#337 of ?” 与 Object 不兼容的 错误动静。
“capture#337 of ?” 暗示什么?当编译器碰着一个在其范例中带有通配符 的变量,好比 rebox() 的 box 参数,它认识到一定有一些 T ,对这些 T 而言 box 是 Box<T>。它不知道 T 代表什么范例,但它可觉得该范例建设一个 占位符来指代 T 的范例。占位符被称为这个非凡通配符的捕捉(capture)。这 种环境下,编译器将名称 “capture#337 of ?” 以 box 范例分派给通配符。 每个变量声明中每呈现一个通配符都将得到一个差异的捕捉,因此在泛型声明 foo(Pair<?,?> x, Pair<?,?> y) 中,编译器将给每四个通配符的 捕捉分派一个差异的名称,因为任意未知的范例参数之间没有干系。
错误动静汇报我们不能挪用 put(),因为它不能检讨 put() 的实参范例与其 形参范例是否兼容 — 因为形参的范例是未知的。在这种环境下,由于 ? 实际 暗示 “?extends Object” ,编译器已经揣度出 box.get() 的范例是 Object ,而不是 “capture#337 of ?”。它不能静态地检讨对由占位符 “capture#337 of ?” 所识此外范例而言 Object 是否是一个可接管的值。
捕捉助手
固然编译器好像扬弃了一些有用的信息,我们可以利用一个能力来使编译器 重构这些信息,即对未知的通配符范例定名。清单 4 展示了 rebox() 的实现和 一个实现这种能力的泛型助手要领(helper):
清单 4. “捕捉助手” 要领
public void rebox(Box<?> box) {
reboxHelper(box);
}
private<V> void reboxHelper(Box<V> box) {
box.put(box.get());
}
助手要领 reboxHelper() 是一个泛型要领,泛型要领引入了特另外范例参数 (位于返回范例之前的尖括号中),这些参数用于暗示参数和/或要领的返回值 之间的范例约束。然而就 reboxHelper() 来说,泛型要领并不利用范例参数指 定范例约束,它答允编译器(通过范例接口)对 box 范例的范例参数定名。
捕捉助手能力答允我们在处理惩罚通配符时绕开编译器的限制。当 rebox() 挪用 reboxHelper() 时,它知道这么做是安详的,因为它自身的 box 参数对一些未 知的 T 而言必然是 Box<T>。因为范例参数 V 被引入到要领签名中而且 没有绑定到其他任何范例参数,它也可以暗示任何未知范例,因此,某些未知 T 的 Box<T> 也大概是某些未知 V 的 Box<V>(这和 lambda 积分中 的 α 减法原则相似,答允重定名界线变量)。此刻 reboxHelper() 中的表达 式 box.get() 不再具有 Object 范例,它具有 V 范例 — 并答允将 V 通报给 Box<V>.put()。
我们原来可以将 rebox() 声明为一个泛型要领,雷同 reboxHelper(),但这 被认为是一种糟糕的 API 设计样式。此处的主要设计原则是 “假如今后毫不会 按名称引用,则不要举办定名”。就泛型要领来说,假如一个范例参数在要领签 名中只呈现一次,它很有大概是一个通配符而不是一个定名的范例参数。一般来 说,带有通配符的 API 比带有泛型要领的 API 更简朴,在更巨大的要领声明中 范例名称的增多会低落声明的可读性。因为在需要时始终可以通过专有的捕捉助 手规复名称,这个要领让您可以或许保持 API 整洁,同时不会删除有用的信息。
范例揣度
捕捉助手能力涉及多个因素:范例揣度和捕捉转换。Java 编译器在许多环境 下都不能执行范例揣度,可是可觉得泛型要领揣度范例参数(其他语言越发依赖 范例揣度,未来我们可以看到 Java 语言中会添加更多的范例揣度特性)。假如 愿意,您可以指定范例参数的值,但只有当您可以或许定名该范例时才可以这样做 — 而且不可以或许暗示捕捉范例。因此要利用这种能力,要求编译器可以或许为您揣度 范例。捕捉转换答允编译器为已捕捉的通配符发生一个占位符范例名,以便对它 举办范例揣度。
当理会一个泛型要领的挪用时,编译器将设法揣度范例参数它能到达的最具 体范例。 譬喻,对付下面这个泛型要领:
public static<T> T identity(T arg) { return arg };
和它的挪用:
Integer i = 3;
System.out.println(identity(i));
编译器可以或许揣度 T 是 Integer、Number、 Serializable 或 Object,但它 选择 Integer 作为满意约束的最详细范例。
当结构泛型实例时,可以利用范例揣度淘汰冗余。譬喻,利用 Box 类建设 Box<String> 要求您指定两次范例参数 String:
Box<String> box = new BoxImpl<String>();
#p#分页标题#e#
纵然可以利用 IDE 执行一些事情,也不要违背 DRY(Don’t Repeat Yourself)原则。然而,假如实现类 BoxImpl 提供一个雷同清单 5 的泛型工场 要领(这始终是个好主意),则可以淘汰客户机代码的冗余:
清单 5. 一个泛型工场要领,可以制止不须腹地指定范例参数
public class BoxImpl<T> implements Box<T> {
public static<V> Box<V> make() {
return new BoxImpl<V>();
}
...
}
假如利用 BoxImpl.make() 工场实例化一个 Box,您只需要指定一次范例参 数:
Box<String> myBox = BoxImpl.make();
泛型 make() 要领为一些范例 V 返回一个 Box<V>,返回值被用于需 要 Box<String> 的上下文中。编译器确定 String 是 V 能接管的满意类 型约束的最详细范例,因此此处将 V 揣度为 String。您还可以手动地指定 V 的值:
Box<String> myBox = BoxImpl.<String>make();
除了淘汰一些键盘操纵以外,此处演示的工场要领能力还提供了优于结构函 数的其他优势:您可以或许为它们提高更具描写性的名称,它们可以或许返回定名返回类 型的子范例,它们不需要为每次挪用建设新的实例,从而可以或许共享不行变的实例 (拜见 参考资料 中的 Effective Java, Item #1,相识有关静态工场的更多优 点)。
竣事语
通配符无疑很是巨大:由 Java 编译器发生的一些令人狐疑的错误动静都与 通配符有关,Java 语言类型中最巨大的部门也与通配符有关。然而假如利用适 当,通配符可以提供强大的成果。此处罗列的两个能力 — 捕捉助手能力和泛型 工场能力 — 都操作了泛型要领和范例揣度,假如利用得当,它们能显著低落复 杂性。