Java 泛型之上界下界通配符
Java 教程是为 JDK 8 编写的。本页描述的示例和实践没有利用后续版本中引入的改进。
通配符和子类型
如 泛型,继承和子类型中所述,泛型类或接口仅仅因为它们的类型之间存在关系而无关。但是,您可以使用通配符在泛型类或接口之间创建关系。
给定以下两个常规(非泛型)类:
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; // 编译时错误
鉴于 Integer
是Number
的子类型,List<Integer>
和 List<Number>
之间的关系是什么?
公共父类是 List<?>
该图表显示 List<Number>
和 List<Integer>
的公共父级是未知类型的List
.
尽管 Integer
是Number
的子类型,但 List<Integer>
不是 List<Number>
的子类型,实际上,这两种类型不相关。List<Number>
和 List<Integer>
的公共父是 List<?>
。
上界 (extends) 的通配符与下界 (super) 通配符
为了在这些类之间创建关系以便代码可以通过 List<Integer>
的元素访问 Number
的方法,请使用上界的通配符:
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK, List<?extends Integer> 是 List< ? extends Number> 的子类型
因为 Integer
是Number
的子类型,而 numList
是Number
对象的列表,所以 intList
(是一个 Integer 对象列表)和numList
之间现在存在关系。下图显示了使用上限和下限通配符声明的多个 List 类之间的关系。
几个通用 List 类声明的层次结构。
图表显示 List<Integer>
是 List <? extends Integer>
和 List<?super Integer>
的子类型。List<? extends Integer>
是 List<? extends Number>
的子类型,它是List <?>
的子类型。List<Number>
是 List <?super Number>
和 List<? extends Number>
的子类型。List<? super Number>
是 List <? super Integer>
的子类型, 且都是 List<?>
的子类型。
通配符使用指南
学习使用泛型编程时,更令人困惑的一个方面是确定何时使用上限有界通配符以及何时使用下限有界通配符。本文提供一些设计代码时要遵循的一些准则。
为讨论方便,认为变量具备两个功能:
一个“In”变量
“in”变量向代码提供数据。想象一下带有两个参数的复制方法:copy(src,dest)。该 SRC 参数提供的数据被复制,因此它是“in”参数。
一个“Out”变量
“out”变量保存数据以供其他地方使用。在复制示例中,copy(src,dest),dest 参数接受数据,因此它是“out”参数。
当然,一些变量既用于“in”又用于“out”目的 – 这种情况也在本文中也用到了。
在决定是否使用通配符以及适合使用哪种类型的通配符时,可以使用“in”和“out”原则。以下列表提供了遵循的准则:
通配符指南:
- 使用
extends
关键字, 定义带有上界通配符的“in”变量。 - 使用
super
关键字,使用下界通配符定义“out”变量。 - 在可以使用
Object
类中定义的方法访问“in”变量的情况下,使用无界通配符。 - 在代码需要作为“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。
- 你可以调用清除。
- 您可以获取迭代器并调用 remove。
- 您可以捕获通配符并写入从列表中读取的元素。
你可以看到 List<? extends NaturalNumber>
在严格意义上不是只读的,但您可能会这样想,因为您无法存储新元素或更改列表中的现有元素。