通配符
上界通配符
您可以使用上界通配符来放宽对变量的限制。例如,假设你想编写一个同时适用于 List<Integer>、List<Double> 和 List<Number> 的方法;你可以通过使用上界通配符来实现。
要声明一个上界通配符,请使用通配符字符(’?‘),后跟 extends 关键字,再后跟其上界。请注意,在此上下文中,extends 以一般意义使用,既表示“extends”(如类),也表示“implements”(如接口)。
要编写处理Number及其子类型(如Integer、Double和Float)列表的方法,应指定List<? extends Number>。List<Number>比List<? extends Number>更具限制性,因为前者仅匹配Number类型的列表,而后者则匹配Number类型及其所有子类的列表。
考虑下面这个process method的例子:
public static void process(List<? extends Foo> list) { /* ... */ }
上界通配符<? extends Foo>(其中 Foo 为任意类型)匹配 Foo 及其所有子类型。process 方法可将列表元素视为类型 Foo :
public static void process(List<? extends Foo> list) {
for (Foo elem : list) {
// ...
}
}
在 foreach 子句中,变量 elem 遍历列表中的每个元素。现在,Foo 类中定义的任何方法都可用于 elem 。
sumOfList() 方法返回列表中数字的总和:
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
以下代码使用一个整数对象列表,输出 sum = 6.0:
List<Integer> li = Arrays.asList(1, 2, 3);
IO.println("sum = " + sumOfList(li));
一个双精度值列表可以使用相同的 sumOfList() 方法。以下代码将输出 sum = 7.0:
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
IO.println("sum = " + sumOfList(ld));
无界通配符
无界通配符类型使用通配符字符(?)指定,例如List<?>。这称为未知类型的列表。无界通配符在两种场景下是有效的解决方案:
- 如果你正在编写一个方法,该方法可以利用Object类提供的功能来实现。
- 当代码使用泛型类中不依赖类型参数的方法时。例如 List.size() 或 List.clear()。事实上,Class<?> 被频繁使用正是因为 Class<T> 中的多数方法并不依赖 T。
请考虑以下方法 printList():
public static void printList(List<Object> list) {
for (Object elem : list)
IO.println(elem + " ");
IO.println();
}
printList() 方法的目标是打印任意类型的列表,但它未能实现这一目标——它仅能打印 Object 实例的列表;无法打印 List<Integer>、List<String>、List<Double> 等类型,因为它们并非 List<Object> 的子类型。要编写泛型 printList() 方法,请使用 List<?>:
public static void printList(List<?> list) {
for (Object elem: list)
IO.print(elem + " ");
IO.println();
}
由于对于任何具体类型 A,List<A> 都是 List<?> 的子类型,因此你可以使用 printList() 方法打印任意类型的列表:
List<Integer> li = Arrays.asList(1, 2, 3);
List<String> ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
注:本节示例中均使用了 Arrays.asList() 方法。该静态工厂方法将指定数组转换为固定大小的列表并返回。
需要特别注意的是,List<Object> 与 List<?> 并非等同。你可以向 List<Object> 中插入 Object 类型或其任何子类型对象,但只能向 List<?> 中插入 null 值。本节末尾的《通配符使用指南》段落提供了更多信息,说明如何根据具体情况判断是否需要使用通配符以及应选用何种通配符。
下界通配符
上界通配符小节说明,上界通配符将未知类型限制为特定类型或该类型的子类型,并使用extends关键字表示。类似地,下界通配符将未知类型限制为特定类型或该类型的超类型。
下界限定通配符通过通配符字符(’?‘)表示,后跟关键字 super ,再接其下界:<? super A>。
注意:您可以为通配符指定上限,也可以指定下限,但不能同时指定两者。
假设你想编写一个方法,将Integer对象放入列表中。为了最大化灵活性,你希望该方法能处理List<Integer>、List<Number>和List<Object>——任何能容纳Integer值的类型。
要编写适用于整数列表及其超类(如Integer、Number和Object)的方法,应指定List<? super Integer>。相较于List<? super Integer>,List<Integer>的限制更严格,因为前者仅匹配整数类型的列表,而后者则匹配任何作为整数超类的列表类型。
以下代码将数字1到10添加到列表末尾:
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
本节末尾的《通配符使用指南》段落提供了关于何时使用上界通配符、何时使用下界通配符的指导原则。
通配符与子类型化
如前文所述,泛型类或接口之间并非仅因其类型存在关联而相关。不过,你可以使用通配符在泛型类或接口之间建立关联关系。
给定以下两个常规(非泛型)类:
class A { /* ... */ }
class B extends A { /* ... */ }
编写以下代码是合理的:
B b = new B();
A a = b;
此示例表明,普通类的继承遵循以下子类型规则:若类 B 扩展类 A,则类 B 是类 A 的子类型。该规则不适用于泛型类型:
List<B> lb = new ArrayList<>();
List<A> la = lb; // compile-time error
鉴于 Integer 是 Number 的子类型,List<Integer> 与 List<Number> 之间是什么关系?
尽管Integer是Number的子类型,但List<Integer>并非List<Number>的子类型,事实上这两种类型并无关联。List<Number>与List<Integer>的共同父类是List<?>。
为在这些类之间建立关联,使代码能够通过 List<Integer> 的元素访问 Number 的方法,请使用上界泛型:
List<? extends Integer> intList = new ArrayList<>();
// This is OK because List<? extends Integer> is a subtype of List<? extends Number>
List<? extends Number> numList = intList;
由于Integer是Number的子类型,而numList是Number对象的列表,因此intList(Integer对象的列表)与numList之间建立了关联。下图展示了使用上下界通配符声明的多个List类之间的关系。
遵循相同规则,List<? extends Number> 可由任何继承自 Number 的类型列表扩展,包括 Number 本身,如下图所示。
同样的情况也适用于 List<? super Integer> 与 List<Integer> 之间的关系。
本节末尾的《通配符使用指南》段落提供了更多关于使用上下限通配符的后果的信息。
通配符捕获与辅助方法
在某些情况下,编译器会推断通配符的类型。例如,列表可能定义为 List<?>,但在评估表达式时,编译器会根据代码推断出特定类型。这种情况称为通配符捕获。
通常情况下,您无需担心通配符捕获的问题,除非看到包含”捕获”字样的错误信息。
编译时,WildcardError 示例会引发捕获错误:
import java.util.List;
public class WildcardError {
void foo(List<?> i) {
i.set(0, i.get(0));
}
}
在此示例中,编译器将输入参数 i 视为 Object 类型。当 foo 方法调用 List.set(int, E) 时,编译器无法确认插入列表的对象类型,从而引发错误。此类错误通常表明编译器认为变量赋值类型不匹配。泛型机制正是为此目的引入Java语言——在编译时强制执行类型安全。
当使用Oracle JDK 7的javac实现进行编译时,WildcardError示例会生成以下错误:
WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
1 error
在此示例中,代码试图执行一项安全操作,那么如何绕过编译器报错?可通过编写捕获通配符的私有辅助方法来修复。具体而言,可创建名为 fooHelper() 的私有辅助方法来解决此问题,具体实现如 WildcardFixed 所示:
public class WildcardFixed {
void foo(List<?> i) {
fooHelper(i);
}
// Helper method created so that the wildcard can be captured
// through type inference.
private <T> void fooHelper(List<T> l) {
l.set(0, l.get(0));
}
}
得益于辅助方法,编译器通过推断确定调用中的 T 是捕获变量 CAP#1。该示例现已成功编译。
按惯例,辅助方法通常命名为 originalMethodNameHelper()。
现在考虑一个更复杂的示例 WildcardErrorBad:
import java.util.List;
public class WildcardErrorBad {
void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
Number temp = l1.get(0);
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
// got a CAP#2 extends Number;
// same bound, but different types
l2.set(0, temp); // expected a CAP#1 extends Number,
// got a Number
}
}
在此示例中,代码正在尝试执行不安全的操作。例如,请考虑以下对 swapFirst() 方法的调用:
List<Integer> li = Arrays.asList(1, 2, 3);
List<Double> ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);
虽然 List<Integer> 和 List<Double> 都满足 List<? extends Number> 的条件,但显然不正确的做法是从一个整型值列表中取出一项,并试图将其放入一个双精度型值列表中。
使用Oracle的JDK javac编译器编译代码时,会产生以下错误:
WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
l2.set(0, temp); // expected a CAP#1 extends Number,
^
required: int,CAP#1
found: int,Number
reason: actual argument Number cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Number from capture of ? extends Number
WildcardErrorBad.java:15: error: method set in interface List<E> cannot be applied to given types;
i.set(0, i.get(0));
^
required: int,CAP#1
found: int,Object
reason: actual argument Object cannot be converted to CAP#1 by method invocation conversion
where E is a type-variable:
E extends Object declared in interface List
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?
3 errors
没有辅助方法可以解决这个问题,因为代码本身存在根本性错误:从整型值列表中提取一项数据并试图将其放入双精度值列表的行为显然是错误的。
通配符使用指南
在学习泛型编程时,最令人困惑的方面之一就是如何判断何时使用上界通配符、何时使用下界通配符。本页面提供了一些在设计代码时应遵循的指导原则。
为便于讨论,可将变量视为提供两种功能之一:
- 一个”输入”变量。输入变量向代码提供数据。想象一个带两个参数的复制方法:copy(src, dest)。src参数提供待复制的数据,因此它是”输入”参数。
- “输出”变量。输出变量用于存储供其他地方使用的数据。在复制示例 copy(src, dest) 中,dest 参数接收数据,因此它是”输出”参数。
当然,某些变量既用于”输入”也用于”输出”——这种情况在指南中也有说明。
在决定是否使用通配符以及选择何种通配符类型时,可遵循”输入”与”输出”原则。以下列表提供了具体指导方针: - “输入” 变量通过使用 extends 关键字,采用上界限定的通配符进行定义。
- 使用 super 关键字定义的 “输出” 变量采用下限限定的通配符。
- 当可通过Object类中定义的方法访问”输入”变量时,请使用无界通配符。
- 当代码需要同时将变量作为”输入”和”输出”变量访问时,请勿使用通配符。
这些准则不适用于方法的返回类型。应避免使用通配符作为返回类型,因为这会迫使使用该代码的程序员处理通配符。
通过 List<? extends …> 定义的列表可非正式地视为只读,但这并非严格保证。假设你有以下两个类:
class NaturalNumber {
private int i;
public NaturalNumber(int i) { this.i = i; }
// ...
}
class EvenNumber extends NaturalNumber {
public EvenNumber(int i) { super(i); }
// ...
}
考虑以下代码段:
List<EvenNumber> le = new ArrayList<>();
List<? extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error
由于 List<EvenNumber> 是 List<? extends NaturalNumber> 的子类型,因此可以将 le 赋值给 ln。但无法使用 ln 将自然数添加到偶数列表中。对该列表可执行的操作包括:
你可以看到,由 List<? extends NaturalNumber> 定义的列表并非严格意义上的只读列表,但你可能会这样认为,因为你无法在列表中存储新元素或修改现有元素。