在泛型代码中,称为通配符的问号 (?) 表示未知类型。 通配符可用于多种情况:作为参数、字段或局部变量的类型; 有时作为返回类型(尽管更具体的是更好的编程实践)。
通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。以下部分更详细地讨论通配符,包括上限通配符、下限通配符和通配符捕获。
1、上限通配符
您可以使用上限通配符来放宽对变量的限制。 例如,假设您想编写一个适用于 List
一点。要声明上限通配符,请使用通配符 ('?'),后跟 extends 关键字,然后是其上限。 请注意,在此上下文中,extends 在一般意义上用于表示“扩展”(如在类中)或“实现”
(如在接口中)。要编写适用于 Number 集合和 Number 子类型(例如 Integer、Double 和 Float)的方法,您需要指定 List 扩展数字>。 术语 List
List extends Number> 因为前者只匹配 Number 类型的集合,而后者匹配 Number 类型的集合或其任何子类。考虑以下处理方法:
public static void process(List extends Foo> list) { /* ... */ }
上限通配符, extends Foo>,其中 Foo 是任何类型,匹配 Foo 和 Foo 的任何子类型。 process 方法可以访问 Foo 类型的集合元素:
public static void process(List extends Foo> list) {
for (Foo elem : list) {
// ...
}
}
在 foreach 子句中,elem 变量迭代集合中的每个元素。 现在可以在 elem 上使用 Foo 类中定义的任何方法。
sumOfList 方法返回集合中数字的总和:
public static double sumOfList(List extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}
以下代码使用 Integer 对象集合,打印 sum = 6.0:
List
System.out.println("sum = " + sumOfList(li));
Double 值集合可以使用相同的 sumOfList 方法。 以下代码打印 sum = 7.0:
List
System.out.println("sum = " + sumOfList(ld));
2、无界通配符
无界通配符类型使用通配符 (?) 指定,例如 List>。 这称为未知类型的集合。 在两种情况下,无界通配符是一种有用的方法:
如果您正在编写可以使用 Object 类中提供的功能实现的方法。
当代码使用不依赖于类型参数的泛型类中的方法时。 例如,List.size 或 List.clear。 事实上,Class> 之所以如此常用,是因为 Class
考虑以下方法,printList:
public static void printList(List
for (Object elem : list)
System.out.println(elem + " ");
System.out.println();
}
printList 的目标是打印任何类型的集合,但它没有达到这个目标——它只打印了一个 Object 实例集合; 它不能打印 List
是 List
public static void printList(List> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
因为对于任何具体类型 A,List 是 List> 的子类型,您可以使用 printList 打印任何类型的集合:
List
List
printList(li);
printList(ls);
需要注意的是 List
了有关如何确定在给定情况下应使用哪种通配符(如果有)的更多信息。
3、下限通配符
上限通配符部分显示上限通配符将未知类型限制为特定类型或该类型的子类型,并使用 extends 关键字表示。 以类似的方式,下限通配符将未知类型限制为特定类型或该类型的超类型。
下限通配符使用通配符 ('?') 表示,后跟 super 关键字,后跟其下限: super A>。
[注意] 您可以为通配符指定上限,也可以指定下限,但不能同时指定两者。
假设您要编写一个将 Integer 对象放入集合的方法。 为了最大限度地提高灵活性,您希望该方法可以处理 List
要编写适用于 Integer 集合和 Integer 超类型(例如 Integer、Number 和 Object)的方法,您需要指定 List super Integer>。 术语 List
配一个 Integer 类型的集合,而后者匹配一个 Integer的 超类型的任何类型的集合。以下代码将数字 1 到 10 添加到列表的末尾:
public static void addNumbers(List super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}
通配符使用指南 部分提供了有关何时使用上限通配符以及何时使用下限通配符的指南。
4、通配符和子类型
如泛型、继承和子类型中所述,泛型类或接口之间的关系不仅仅因为它们的类型之间存在关系。 但是,您可以使用通配符来创建通用类或接口之间的关系。给定以下两个常规(非通用)类:
class A { /* ... */ }
class B extends A { /* ... */ }
下面代码是允许的:
B b = new B();
A a = b;
此示例显示常规类的继承遵循此子类型规则:如果 B 继承 A,则类 B 是类 A 的子类型。此规则不适用于泛型类型:
List lb = new ArrayList<>();
List la = lb; // compile-time error
鉴于 Integer 是 Number 的子类型,List
虽然 Integer 是 Number 的子类型,但 List
为了在这些类之间创建关系,以便代码可以通过 List
List extends Integer> intList = new ArrayList<>();
List extends Number> numList = intList; // OK. List extends Integer> is a subtype of List extends Number>
因为 Integer 是 Number 的子类型,而 numList 是 Number 对象的集合,所以现在 intList(Integer 对象的集合)和 numList 之间存在关系。 下图显示了使用上限和下限通
配符声明的几个 List 类之间的关系。
几个通用 List 类声明的层次结构。
5、通配符捕获和辅助方法
在某些情况下,编译器会推断通配符的类型。 例如,一个列表可能被定义为 List> 但是,当评估一个表达式时,编译器会从代码中推断出一个特定的类型。 这种情况称为
通配符捕获。大多数情况下,您无需担心通配符捕获,除非您看到包含短语“capture of”的错误消息。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 语言中——以在编译时强制执行类型安全。WildcardError 示例在由 Oracle 的 JDK 7
javac 实现编译时会生成以下错误:
WildcardError.java:6: error: method set in interface List
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
l.set(0, l.get(0));
}
}
多亏了 helper 方法,编译器在调用中使用推理来确定 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
List
swapFirst(li, ld);
而 List
javac 编译器编译代码会产生以下错误:
WildcardErrorBad.java:7: error: method set in interface List
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
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
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
没有解决此问题的辅助方法,因为代码从根本上是错误的:从 Integer 值集合中取出一个项目并将其放入 Double 值集合中显然是不正确的。
6、通配符使用指南
在学习使用泛型编程时,更令人困惑的方面之一是确定何时使用上限通配符以及何时使用下限通配符。 此页面提供了设计代码时要遵循的一些准则。出于本次讨论的目的,
将变量视为提供以下两个功能之一是有帮助的:方法入参变量(in) “in”变量为代码提供数据。 想象一个带有两个参数的复制方法:copy(src, dest)。 src 参数提供要复制
的数据,因此它是“in”参数。 方法出参变量(out) “out”变量保存用于其他地方的数据。 在复制示例 copy(src, dest) 中,dest 参数接受数据,因此它是“out”参数。当然,
有些变量同时用于“输入”和“输出”的目的——指南中也提到了这种情况。在决定是否使用通配符以及哪种类型的通配符合适时,您可以使用“输入”和“输出”原则。 以下列表
提供了要遵循的准则:
一个“in”变量是用一个上限通配符定义的,使用 extends 关键字。
"out" 变量使用下界通配符定义,使用 super 关键字。
如果可以使用 Object 类中定义的方法访问“in”变量,请使用无界通配符。
在代码需要访问变量作为“输入”和“输出”变量的情况下,不要使用通配符。
这些准则不适用于方法的返回类型。 应该避免使用通配符作为返回类型,因为它迫使程序员使用代码来处理通配符。一个集合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
List extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // compile-time error
因为 List
可以添加空值。
可以调用clear。
可以获取迭代器并调用remove。
可以捕获通配符并写入从列表中读取的元素。
因为无法存储新元素或更改列表中的现有元素,可能会认为 List extends NaturalNumber> 是只读的,但在严格意义上不是只读的。