泛型通配符
在泛型代码中,称为通配符的问号(?
)表示未知类型,通配符可用于各种情况:作为参数、字段或局部变量的类型,有时作为返回类型(尽管更好的编程实践是更加具体),通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。
以下部分更详细地讨论通配符,包括上界通配符、下界通配符和通配符捕获。
上界通配符
你可以使用上界通配符来放宽对变量的限制,例如,假设你要编写一个适用于List<Integer>
、List<Double>
和List<Number>
的方法,你可以通过使用上界通配符来实现这一点。
要声明一个上界通配符,请使用通配符('?
'),后跟extends
关键字,后跟上界,请注意,在此上下文中,extends
在一般意义上用于表示“extends”(如在类中)或“implements”(如在接口中)。
要编写适用于Number
和Number
的子类型列表的方法,例如Integer
、Double
和Float
,你可以指定List<?extends Number>
,List<Number>
一词比List<? extends Number>
更具限制性,因为前者只匹配Number
类型的列表,而后者匹配Number
类型或其任何子类的列表。
考虑以下process
方法:
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));
无界通配符
使用通配符(?
)指定无界通配符类型,例如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);
在本课程的示例中使用了Arrays.asList方法,此静态工厂方法转换指定的数组并返回固定大小的列表。
重要的是要注意List<Object>
和List<?>
是不一样的,你可以将Object
或Object
的任何子类型插入List<Object>
,但是你只能在List<?>
中插入null
,通配符使用指南部分提供了有关如何确定在给定情况下应使用哪种通配符(如果有)的更多信息。
下界通配符
上界通配符部分显示上界通配符将未知类型限制为该类型的特定类型或子类型,并使用extends
关键字表示,以类似的方式,下界通配符将未知类型限制为该类型的特定类型或超类型。
使用通配符(?
)表示下界通配符,后跟super
关键字,后跟下界:<? super A>
。
你可以指定通配符的上界,也可以指定下界限,但不能同时指定两者。
假设你要编写一个将Integer
对象放入列表的方法,为了最大限度地提高灵活性,你希望该方法可以处理List<Integer>
、List<Number>
和List<Object>
— 任何可以保存Integer
值的方法。
要编写适用于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);
}
}
通配符和子类型
如泛型、继承和子类型中所述,泛型类或接口不相关,仅仅因为他们的类型之间存在关系,但是,你可以使用通配符在泛型类或接口之间创建关系。
给定以下两个常规(非泛型)类:
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
类之间的关系。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。