目录
1、如何定义和使用上界通配符?
2、如何定义和使用无界通配符?
3、如何定义和使用下界通配符?
4、如何使用通配符定义泛型类或接口之间的子类型关系?
5、通配符的捕获和辅助方法
6、通配符使用指南
在泛型代码中,问号(?)称为通配符,用来表示未知类型。通配符可以在多种情况下使用:如作为参数、字段或局部变量的类型;有时也可以作为返回类型。另外,通配符永远不会用作调用泛型方法、创建泛型类或超类型实例的类型参数。
为什么使用通配符?
在 Java 中,类和数组之间的对象关系是可以继承的,比如 Dog extends Animal,那么 Animal[] 与 Dog[] 就是兼容的。但是集合之间却不存在这种关系,也就是说 List
可以使用上界通配符来放宽对变量的限制。例如,你想编写一个方法,该方法适用于List
声明一个上界通配符,需要使用通配符 ('?'),跟上 extends 关键字,然后再跟它的上界。比如,编写用于 Number 列表和 Number 子类型(如 Integer、Double 和 Float)的方法,可以指定 List extends Number>。List extends Number> 要比 List
例如下边的程序代码:
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 li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));
Double 值列表可以使用相同的 sumOfList() 方法。下面的代码输出 sum = 7.0:
List ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));
无界通配符类型使用通配符(?)指定,例如 List>。在下边两种情况下,无界通配符是一种有用的方法:
例如以下 printList() 方法:
public static void printList(List
printList() 的目标是用来打印任何类型的列表,但上述代码中它只能打印 Object 实例的列表;并不能打印 List
public static void printList(List> list) {
for (Object elem: list)
System.out.print(elem + " ");
System.out.println();
}
对于任何具体的类型 A,List 都是 List> 的子类,可以使用 printList() 打印任何类型的列表:// 解决了之前 List 和 List 无任何关系的问题
List li = Arrays.asList(1, 2, 3);
List ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
注意:List
上界通配符将未知类型限制为特定类型的子类型,使用 extends 关键字表示。类似地,下界通配符将未知类型限制为特定类型的超类型,使用 super 关键字表示。
下界通配符使用通配符('?')表示,后面跟着 super 关键字,然后再跟着它的下界: super A>。
注意:可以为通配符指定上界,也可以指定下界,但不能同时都指定。
例如,编写一个将 Integer 对象放入列表的方法。为了最大限度地提高灵活性,该方法需要适用于 List
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.calss 就是 A.calss 的子类型。但是这个规则并不适用于泛型类型:
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>,但是,当计算表达式时,编译器会从代码中推断出特定的类型,这种场景称为通配符捕获。
大多数情况下,都不需要担心通配符的捕获,除非看到包含短语 “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 语言中就是出于这个原因——在编译时加强类型安全。
当使用 Oracle 的 JDK 7 javac 实现编译时,WildcardError 示例会生成以下错误:
WildcardError.java:6: error: method set in interface List 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
在本例中,代码试图执行安全操作,那么如何解决编译器错误呢?可以通过编写一个捕获通配符的私有 helper 方法来修复它。如 WildcardFixed 所示,通过创建私有辅助方法 fooHelper 来解决这个问题:
public class WildcardFixed {
void foo(List> i) {
fooHelper(i);
}
// 创建辅助方法,以便可以通过类型推断捕获通配符
private void fooHelper(List l) {
l.set(0, l.get(0));
}
}
由于使用了 helper 方法,编译器使用推断来确定调用中的 T 是 CAP#1,即捕获变量。示例现在编译成功。
在使用泛型进行编程时,让人比较困惑的是需要确定何时使用上界通配符,何时使用下界通配符。本节提供了一些设计代码时要遵循的指导原则。// 以下是来自官方的指导原则
为了方便理解,可以将变量看作以下两种形式:
当然,有些变量同时用于 “输入” 和 “输出” 目的,下边指南中也提到了这种情况。
在决定是否使用通配符以及使用什么类型的通配符时,可以使用 “in” 和 “out” 原则。以下列表提供了需要遵循的指导方针:
以上这些准则不适用于方法的返回类型。应该避免使用通配符作为方法的返回类型,因为它会迫使使用代码的程序员去处理通配符。
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 le = new ArrayList<>();
List extends NaturalNumber> ln = le;
ln.add(new NaturalNumber(35)); // 编译错误
因为 List
所以,从严格意义上来讲,List extends NaturalNumber> 并不是只读的,但是却可以认为它是只读的,因为不能在列表中存储新的元素或更改现有元素。