夏眠鱼

Oct 24, 2017

Java 泛型-通配符

在泛型代码中,问号(?)叫做通配符,代表未知类型。通配符可以在各种情况下使用:作为参数、字段或局部变量的类型;有时作为返回类型(虽然更具体的编程实践是更好的)。通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。

下面的小节将更详细地讨论通配符,包括上界通配符、无界通配符、下界通配符等。

上界通配符

我们可以使用上界通配符来放松对变量的限制。比如我们想要编写一个在 List<Integer>List<Double>List<Number> 中工作的方法,就可以通过使用上界通配符来实现。

一个上界通配符使用通配符字符(?)表示,然后跟着是 extends 关键字,最后跟着是它的上界。注意,在这里 extends 通常用于表示 extends(如在类中)或 implements(如在接口中)。

为了编写一个方法,它的参数为 List<Number>List<Number 的子类> ,我们应该指定方法的参数类型为 List<? extends Number>

思考下面的 process 方法:

1
public static void process(List<? extends Foo> list) { /* ... */ }

上界通配符 <? extends Foo>,匹配任何 Foo 或 Foo 子类型,这样 process 方法能以 Foo 类型来访问集合中的元素:

1
2
3
4
5
public static void process(List<? extends Foo> list) {
for (Foo elem : list) {
// ...
}
}

在 foreach 语句中,变量 elem 遍历集合中的每个元素。定义在 Foo 类中的任何方法现在都能在 elem 上使用。

我们再看下面一个例子,sumOfList 方法可以返回集合中 Number 的数量:

1
2
3
4
5
6
public static double sumOfList(List<? extends Number> list) {
double s = 0.0;
for (Number n : list)
s += n.doubleValue();
return s;
}

无界通配符

无界通配符是使用通配符(?)指定的。例如,List<?>,这个叫未知类型的集合。在下面这两种场景中,无界通配符是一种有用的方法:

  • 如果您正在编写一个方法,可以使用 Object 类来实现提供的功能
  • 在泛型类中使用方法时,不需要依赖类型参数。例如,List.size 或 List.clear

思考下面的 printList 方法:

1
2
3
4
5
6
7
public static void printList(List<Object> list) {
for (Object elem : list) {
System.out.println(elem + " ");
}

System.out.println();
}

printList 的目标是打印任何类型的列表,但是它不能实现这一目标,只能打印 Object 实例的列表。它不能打印 List<Integer>List<String>List<Double> 等等,因为她们不是 List<Object> 的子类型。为了实现通用化的 printList,使用 List<?>

1
2
3
4
5
6
7
public static void printList(List<?> list) {
for (Object elem: list) {
System.out.print(elem + " ");
}

System.out.println();
}

需要注意的是,List<Object>List<?> 是不一样的。我们可以向 List<Object> 对象中插入一个 Object 或任何 Object 子类的对象,但是我们只能向 List<?> 中插入 null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Box<T> {

private T t;

public void set(T t) {
this.t = t;
}

public T get() {
return t;
}
}

Box<?> boxs = new Box<>();
box.set(null); // 编译正确
box.set("hello"); // 编译错误

下界通配符

与上界通配符相似,下界通配符将未知类型限制为指定类型或指定类型的父类。一个下界通配符使用通配符字符(?)表示,然后跟着是 super 关键字,最后跟着是它的下界。

注意,你可以为通配符指定一个上限,或者您可以指定一个下界,但不能同时指定两者。

假设你想编写一个方法,将 Integer 对象放入一个列表中。为了最大化灵活化,你希望该方法在 List<Integer>List<Number>List<Object> 上工作。

要编写参数类型为 Integer 和 Integer 父类的方法,例如 Integer、Number 和 Object,您将指定 List <? super Integer>List<Integer>List<? super Integer> 更限制,因为前者只匹配 Integer 类型,而后者匹配任何 Integer 父类的类型。

下面的代码将数字 1 到 10 添加到列表的末尾:

1
2
3
4
5
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}

通配符和子类型

正如 Java 泛型-继承和子类型 中描述一样,泛型类或接口是没有联系的,只是因为它们的类型之间存在关系。然而,使用通配符来创建泛型类或接口之间的关系。

下面是两个常规(非泛型)的类:

1
2
class A { /* ... */ }
class B extends A { /* ... */ }

以下代码是合理的:

1
2
B b = new B();
A a = b;

上面的例子表明常规类的继承遵循子类型规则:如果 B 是 A 的子类,那么 B 是 A 的子类型。但是这个规则不适用在泛型类型中:

1
2
List<B> lb = new ArrayList<>();
List<A> la = lb; // 编译错误

给定 Integer 是 Number 的子类型,那么 List<Integer>List<Number> 如下:

generics-list-parent

虽然 Integer 是 Number 的子类型,但 List<Integer> 不是 List<Number> 的子类型,实际上,它们是不相关的。List<Integer>List<Number> 的公共父类是 List<?>

为了让 List<Integer>List<Number> 产生关系,我们可以使用一个上界通配符:

1
2
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // 正确,因为 List<? extends Integer> 是 List<? extends Number> 的子类型

因为 Integer 是 Number 的子类型,numList 是 Number 对象的集合,所以现在 intList 和 numList 建立了关系。

下面的关系图显示了几个已声明为上下有界通配符的列表类之间的关系:

generics-wildcard-subtyping

OLDER > < NEWER