Java通配符?
通配符基本介绍
泛型是⼀种表⽰类或⽅法⾏为对于未知类型的类型约束的⽅法,⽐如 “不管这个⽅法的参数 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.());
}
事实证明 Unbox ⽅法能做许多⼯作:它能调⽤ get() ⽅法,并且能调⽤任何从 Object 继承⽽来的⽅法(⽐如 hashCode())。它惟⼀不能做的事是调⽤ put() ⽅法,这是因为在不知道该 Box 实例的类型参数 T 的情况下它不能检验这个操作的安全性。由于 box 是⼀个
Box<?> ⽽不是⼀个原始的 Box,编译器知道存在⼀些 T 充当 box 的类型参数,但由于不知道 T 具体是什么,您不能调⽤ put() 因为不能检验这么做不会违反 Box 的类型安全限制(实际上,您可以在⼀
个特殊的情况下调⽤ put():当您传递 null 字母时。我们可能不知道 T 类型代表什么,但我们知道 null 字母对任何引⽤类型⽽⾔是⼀个空值)。
关于 () 的返回类型,unbox() 了解哪些内容呢?它知道 () 是某些未知 T 的 T,因此它可以推断出 get() 的返回类型是 T 的擦除(erasure),对于⼀个⽆上限的通配符就是 Object。因此清单 2 中的表达式 () 具有 Object 类型。
通配符捕获
清单 3 展⽰了⼀些似乎应该可以⼯作的代码,但实际上不能。它包含⼀个泛型 Box、提取它的值并试图将值放回同⼀个 Box。
清单 3. ⼀旦将值从 box 中取出,则不能将其放回
public void rebox(Box<?> box) {
box.());
}
Rebox.java:8: put(capture#337 of ?) in Box<capture#337 of ?> cannot be applied
to (java.lang.Object)
box.());
^
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” ,编译器已经推断出
() 的类型是 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.());
}
助⼿⽅法 reboxHelper() 是⼀个泛型⽅法,泛型⽅法引⼊了额外的类型参数(位于返回类型之前的尖括
号中),这些参数⽤于表⽰参数和/或⽅法的返回值之间的类型约束。然⽽就 reboxHelper() 来说,泛型⽅法并不使⽤类型参数指定类型约束,它允许编译器(通过类型接⼝)对 box 类型的类型参数命名。
捕获助⼿技巧允许我们在处理通配符时绕开编译器的限制。当 rebox() 调⽤ reboxHelper() 时,它知道这么做是安全的,因为它⾃⾝的box 参数对⼀些未知的 T ⽽⾔⼀定是 Box<T>。因为类型参数 V 被引⼊到⽅法签名中并且没有绑定到其他任何类型参数,它也可以表⽰任何未知类型,因此,某些未知 T 的 Box<T> 也可能是某些未知 V 的 Box<V>(这和 lambda 积分中的 α 减法原则相似,允许重命名边界变量)。现在 reboxHelper() 中的表达式 () 不再具有 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>();
即使可以使⽤ 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();
java编译器ide最新版下载泛型 make() ⽅法为⼀些类型 V 返回⼀个 Box<V>,返回值被⽤于需要 Box<String> 的上下⽂中。编译器确定 String 是 V 能接受的满⾜类型约束的最具体类型,因此此处将 V 推断为 String。您还可以⼿动地指定 V 的值:
Box<String> myBox = BoxImpl.<String>make();
除了减少⼀些键盘操作以外,此处演⽰的⼯⼚⽅法技巧还提供了优于构造函数的其他优势:您能够为它们提⾼更具描述性的名称,它们能够返回命名返回类型的⼦类型,它们不需要为每次调⽤创建新的实例,从⽽能够共享不可变的实例(参见参考资料中的 Effective Java, Item #1,了解有关静态⼯⼚的更多优点)。
结束语
通配符⽆疑⾮常复杂:由 Java 编译器产⽣的⼀些令⼈困惑的错误消息都与通配符有关,Java 语⾔规范中最复杂的部分也与通配符有关。然⽽如果使⽤适当,通配符可以提供强⼤的功能。此处列举的两个技巧 — 捕获助⼿技巧和泛型⼯⼚技巧 — 都利⽤了泛型⽅法和类型推断,如果使⽤恰当,它们能显
著降低复杂性。