系列中的上一篇
当前教程
通配符
系列中的下一篇

系列中的上一篇: 类型推断

系列中的下一篇: 类型擦除

通配符

 

上界通配符

您可以使用上界通配符来放宽对变量的限制。例如,假设您想编写一个适用于List<Integer>List<Double>List<Number>的方法;您可以通过使用上界通配符来实现这一点。

要声明上界通配符,请使用通配符字符('?'),后跟extends关键字,最后是其上界。请注意,在此上下文中,extends以一般意义使用,表示“extends”(如在类中)或“implements”(如在接口中)。

要编写适用于Number列表和Number子类型(如IntegerDoubleFloat)列表的方法,您将指定List<? extends Number>。术语List<Number>List<? extends Number>更严格,因为前者仅匹配类型为Number的列表,而后者匹配类型为Number或其任何子类的列表。

考虑以下过程方法

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变量遍历列表中的每个元素。现在,可以在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<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

Double值列表可以使用相同的sumOfList()方法。以下代码打印sum = 7.0

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.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)
        System.out.println(elem + " ");
    System.out.println();
}

printList()的目标是打印任何类型的列表,但它未能实现该目标——它只打印Object实例的列表;它无法打印List<Integer>List<String>List<Double>等,因为它们不是List<Object>的子类型。要编写泛型printList()方法,请使用List<?>

public static void printList(List<?> list) {
    for (Object elem: list)
        System.out.print(elem + " ");
    System.out.println();
}

因为对于任何具体类型AList<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<?>并不相同。您可以将ObjectObject的任何子类型插入List<Object>中。但是,您只能将null插入List<?>中。本节末尾的“通配符使用指南”段落提供了有关如何在给定情况下确定使用哪种通配符(如果有)的更多信息。

 

下界通配符

“上界通配符”部分显示,上界通配符将未知类型限制为特定类型或该类型的子类型,并使用extends关键字表示。类似地,下界通配符将未知类型限制为特定类型或该类型的超类型。

下界通配符使用通配符字符('?')表示,后跟super关键字,最后是其下界:<? super A>

注意:您可以为通配符指定上界,也可以指定下界,但不能同时指定两者。

假设您想编写一个将Integer对象放入列表中的方法。为了最大限度地提高灵活性,您希望该方法适用于List<Integer>List<Number>List<Object>——任何可以容纳Integer值的东西。

要编写适用于Integer列表和Integer超类型(如IntegerNumberObject)列表的方法,您将指定List<? super Integer>。术语List<Integer>List<? super Integer>更严格,因为前者仅匹配类型为Integer的列表,而后者匹配任何类型为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

鉴于IntegerNumber的子类型,那么List<Integer>List<Number>之间是什么关系?

The common parent parameterized lists

常见的父参数化列表。

尽管IntegerNumber的子类型,但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;  

因为 IntegerNumber 的子类型,而 numList 是一个包含 Number 对象的列表,所以 intList(一个包含 Integer 对象的列表)和 numList 之间现在存在着关系。下图显示了用上界和下界通配符声明的几个 List 类之间的关系。

A hierarchy of several generic List class declarations

几个泛型 List 类声明的层次结构。

遵循相同的规则,List<? extends Number> 可以被任何扩展 Number 的类型的列表扩展,包括 Number 本身,如下图所示。

A list of Number extends a list of ? extends Number

一个 Number 列表扩展了一个 ? extends Number 列表。

List<? super Integer>List<Integer> 之间的关系也是如此。

A list of Integer extends a list of ? super Integer

一个 Integer 列表扩展了一个 ? super Integer 列表。

本节末尾的 通配符使用指南 段落提供了有关使用上界和下界通配符的影响的更多信息。

 

通配符捕获和辅助方法

在某些情况下,编译器会推断通配符的类型。例如,一个列表可以定义为 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<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));
    }

}

由于辅助方法的存在,编译器使用推断来确定 TCAP#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> 的条件,但从一个包含 Integer 值的列表中取出一个项目并试图将其放入一个包含 Double 值的列表中显然是不正确的。

使用 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

没有辅助方法可以解决这个问题,因为代码从根本上是错误的:从一个包含 Integer 值的列表中取出一个项目并试图将其放入一个包含 Double 值的列表中显然是不正确的。

 

通配符使用指南

学习使用泛型编程时,最令人困惑的方面之一是确定何时使用上界通配符,何时使用下界通配符。本页提供了一些在设计代码时应遵循的指南。

为了便于讨论,将变量视为提供以下两种功能之一是有帮助的

  • "In" 变量。一个“in”变量为代码提供数据。想象一个有两个参数的复制方法:copy(src, dest)src 参数提供要复制的数据,因此它是“in”参数。
  • "Out" 变量。一个“out”变量保存数据以供其他地方使用。在复制示例中,copy(src, dest)dest 参数接受数据,因此它是“out”参数。

当然,有些变量既用于“in”目的,也用于“out”目的——本指南中也讨论了这种情况。

在决定是否使用通配符以及使用哪种类型的通配符时,可以使用“in”和“out”原则。以下列表提供了应遵循的指南

  • 一个“in”变量用上界通配符定义,使用 extends 关键字。
  • 一个“out”变量用下界通配符定义,使用 super 关键字。
  • 如果“in”变量可以使用在 Object 类中定义的方法访问,则使用无界通配符。
  • 如果代码需要将变量作为“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<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 将自然数添加到偶数列表中。以下列表上的操作是可能的

  • 您可以添加 null
  • 您可以调用 clear().
  • 您可以获取迭代器并调用 remove().
  • 您可以捕获通配符并写入从列表中读取的元素。

您可以看到由 List<? extends NaturalNumber> 定义的列表并非严格意义上的只读,但您可能会这样认为,因为您不能在列表中存储新元素或更改现有元素。


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
通配符
系列中的下一篇

系列中的上一篇: 类型推断

系列中的下一篇: 类型擦除