3

泛型通配符

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

以下部分更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。

上界通配符

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

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

要编写适用于NumberNumber的子类型列表的方法,例如IntegerDoubleFloat,你可以指定List<?extends Number>List<Number>一词比List<? extends Number>更具限制性,因为前者只匹配Number类型的列表,而后者匹配Number类型或其任何子类的列表。

考虑以下process方法:

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

上界通配符<? extends Foo>,其中Foo是任何类型,匹配FooFoo的任何子类型,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.sizeList.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>,但是你只能在List<?>中插入null,通配符使用指南部分提供了有关如何确定在给定情况下应使用哪种通配符(如果有)的更多信息。

下界通配符

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

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

你可以指定通配符的上界,也可以指定下界限,但不能同时指定两者。

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

要编写适用于IntegerInteger超类型列表的方法,例如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>之间的关系是什么?

generics-listParent.gif

虽然IntegerNumber的子类型,但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>

因为IntegerNumber的子类型,而numListNumber对象的列表,所以intListInteger对象列表)和numList之间现在存在关系,下图显示了使用上界和下界通配符声明的多个List类之间的关系。

generics-wildcardSubtyping.gif


上一篇:类型推断
下一篇:泛型通配符捕获和Helper方法

博弈
2.5k 声望1.5k 粉丝

态度决定一切