java泛型系列 | 七、通配符

在泛型代码中,称为通配符的问号 (?) 表示未知类型。 通配符可用于多种情况:作为参数、字段或局部变量的类型; 有时作为返回类型(尽管更具体的是更好的编程实践)。

通配符永远不会用作泛型方法调用、泛型类实例创建或超类型的类型参数。以下部分更详细地讨论通配符,包括上限通配符、下限通配符和通配符捕获。

1、上限通配符

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

一点。要声明上限通配符,请使用通配符 ('?'),后跟 extends 关键字,然后是其上限。 请注意,在此上下文中,extends 在一般意义上用于表示“扩展”(如在类中)或“实现”

(如在接口中)。要编写适用于 Number 集合和 Number 子类型(例如 Integer、Double 和 Float)的方法,您需要指定 List<? 扩展数字>。 术语 List<Number>

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<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));

2、无界通配符

无界通配符类型使用通配符 (?) 指定,例如 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();
}

因为对于任何具体类型 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);

需要注意的是 List<Object> 和 List<?> 并不相同。 您可以将对象或对象的任何子类型插入到 List<Object> 中。 但是你只能在 List<?> 中插入 null。 通配符 使用指南部分提供

了有关如何确定在给定情况下应使用哪种通配符(如果有)的更多信息。

3、下限通配符

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

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

[注意​]  您可以为通配符指定上限,也可以指定下限,但不能同时指定两者。

假设您要编写一个将 Integer 对象放入集合的方法。 为了最大限度地提高灵活性,您希望该方法可以处理 List<Integer>、List<Number> 和 List<Object>——任何可以保存整数值的东西。

要编写适用于 Integer 集合和 Integer 超类型(例如 Integer、Number 和 Object)的方法,您需要指定 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);
    }
}

通配符使用指南 部分提供了有关何时使用上限通配符以及何时使用下限通配符的指南。

4、通配符和子类型

如泛型、继承和子类型中所述,泛型类或接口之间的关系不仅仅因为它们的类型之间存在关系。 但是,您可以使用通配符来创建通用类或接口之间的关系。给定以下两个常规(非通用)类:

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<>();
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<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));
    }
}

多亏了 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<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 值集合中显然是不正确的。

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<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 将自然数添加到偶数集合中。 可以对列表进行以下操作:

  • 可以添加空值。

  • 可以调用clear。

  • 可以获取迭代器并调用remove。

  • 可以捕获通配符并写入从列表中读取的元素。

因为无法存储新元素或更改列表中的现有元素,可能会认为 List<? extends NaturalNumber> 是只读的,但在严格意义上不是只读的。

 

 

 

 

posted @ 2021-09-16 16:29  meow_world  阅读(925)  评论(0)    收藏  举报