边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends
关键字。
重要的是要理解,当用于限定泛型类型时,extends
的含义与通常的意义截然不同。此示例展示边界的基础应用:
interface HasColor {
java.awt.Color getColor();
}
class WithColor<T extends HasColor> {
T item;
WithColor(T item) {
this.item = item;
}
T getItem() {
return item;
}
// The bound allows you to call a method:
java.awt.Color color() {
return item.getColor();
}
}
class Coord {
public int x, y, z;
}
// This fails. Class must be first, then interfaces:
// class WithColorCoord {
// Multiple bounds:
class WithColorCoord<T extends Coord & HasColor> {
T item;
WithColorCoord(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
interface Weight {
int weight();
}
// As with inheritance, you can have only one
// concrete class but multiple interfaces:
class Solid<T extends Coord & HasColor & Weight> {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
java.awt.Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded
extends Coord implements HasColor, Weight {
@Override
public java.awt.Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<>(new Bounded());
solid.color();
solid.getY();
solid.weight();
}
}
你可能会观察到 BasicBounds.java 中似乎包含一些冗余,它们可以通过继承来消除。在这里,每个继承级别还添加了边界约束:
class HoldItem<T> {
T item;
HoldItem(T item) {
this.item = item;
}
T getItem() {
return item;
}
}
class WithColor2<T extends HasColor>
extends HoldItem<T> {
WithColor2(T item) {
super(item);
}
java.awt.Color color() {
return item.getColor();
}
}
class WithColorCoord2<T extends Coord & HasColor>
extends WithColor2<T> {
WithColorCoord2(T item) {
super(item);
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
class Solid2<T extends Coord & HasColor & Weight>
extends WithColorCoord2<T> {
Solid2(T item) {
super(item);
}
int weight() {
return item.weight();
}
}
public class InheritBounds {
public static void main(String[] args) {
Solid2<Bounded> solid2 = new Solid2<>(new Bounded());
solid2.color();
solid2.getY();
solid2.weight();
}
}
HoldItem 拥有一个对象,因此此行为将继承到 WithColor2 中,这也需要其参数符合 HasColor。 WithColorCoord2 和 Solid2 进一步扩展了层次结构,并在每个级别添加了边界。现在,这些方法已被继承,并且在每个类中不再重复。
这是一个具有更多层次的示例:
import java.util.List;
interface SuperPower {
}
interface XRayVision extends SuperPower {
void seeThroughWalls();
}
interface SuperHearing extends SuperPower {
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower {
void trackBySmell();
}
class SuperHero<POWER extends SuperPower> {
POWER power;
SuperHero(POWER power) {
this.power = power;
}
POWER getPower() {
return power;
}
}
class SuperSleuth<POWER extends XRayVision>
extends SuperHero<POWER> {
SuperSleuth(POWER power) {
super(power);
}
void see() {
power.seeThroughWalls();
}
}
class
CanineHero<POWER extends SuperHearing & SuperSmell>
extends SuperHero<POWER> {
CanineHero(POWER power) {
super(power);
}
void hear() {
power.hearSubtleNoises();
}
void smell() {
power.trackBySmell();
}
}
class SuperHearSmell
implements SuperHearing, SuperSmell {
@Override
public void hearSubtleNoises() {
}
@Override
public void trackBySmell() {
}
}
class DogPerson extends CanineHero<SuperHearSmell> {
DogPerson() {
super(new SuperHearSmell());
}
}
public class EpicBattle {
// Bounds in generic methods:
static <POWER extends SuperHearing>
void useSuperHearing(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
}
static <POWER extends SuperHearing & SuperSmell>
void superFind(SuperHero<POWER> hero) {
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogPerson dogPerson = new DogPerson();
useSuperHearing(dogPerson);
superFind(dogPerson);
// You can do this:
List<? extends SuperHearing> audioPeople;
// But you can't do this:
// List dogPs;
}
}
接下来将要研究的通配符将会把范围限制在单个类型。
你已经在 集合
章节中看到了一些简单示例使用了通配符——在泛型参数表达式中的问号,在 类型信息
一章中这种示例更多。本节将更深入地探讨这个特性。
我们的起始示例要展示数组的一种特殊行为:你可以将派生类的数组赋值给基类的引用:
class Fruit {
}
class Apple extends Fruit {
}
class Jonathan extends Apple {
}
class Orange extends Fruit {
}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
fruit[1] = new Jonathan(); // OK
// Runtime type is Apple[], not Fruit[] or Orange[]:
try {
// Compiler allows you to add Fruit:
fruit[0] = new Fruit(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
try {
// Compiler allows you to add Oranges:
fruit[0] = new Orange(); // ArrayStoreException
} catch (Exception e) {
System.out.println(e);
}
}
}
main()
中的第一行创建了 Apple 数组,并赋值给一个 Fruit 数组引用。这是有意义的,因为 Apple 也是一种 Fruit,因此 Apple 数组应该也是一个 Fruit 数组。
但是,如果实际的数组类型是 Apple[],你可以在其中放置 Apple 或 Apple 的子类型,这在编译期和运行时都可以工作。但是你也可以在数组中放置 Fruit 对象。这对编译器来说是有意义的,因为它有一个 Fruit[] 引用——它有什么理由不允许将 Fruit 对象或任何从 Fruit 继承出来的对象(比如 Orange),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。
向上转型用在这里不合适。你真正在做的是将一个数组赋值给另一个数组。数组的行为是持有其他对象,这里只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。看起来就像数组对它们持有的对象是有意识的,因此在编译期检查和运行时检查之间,你不能滥用它们。
数组的这种赋值并不是那么可怕,因为在运行时你可以发现插入了错误的类型。但是泛型的主要目标之一是将这种错误检测移到编译期。所以当我们试图使用泛型集合代替数组时,会发生什么呢?
import java.util.*;
public class NonCovariantGenerics {
// Compile Error: incompatible types:
List<Fruit> flist = new ArrayList<Apple>();
}
尽管你在首次阅读这段代码时会认为“不能将一个 Apple 集合赋值给一个 Fruit 集合”。记住,泛型不仅仅是关于集合,它真正要表达的是“不能把一个涉及 Apple 的泛型赋值给一个涉及 Fruit 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会留下一些余地。
但是它不知道任何有关这方面的信息,因此它拒绝向上转型。然而实际上这也不是向上转型—— Apple 的 List 不是 Fruit 的 List。Apple 的 List 将持有 Apple 和 Apple 的子类型,Fruit 的 List 将持有任何类型的 Fruit。是的,这包括 Apple,但是它不是一个 Apple 的 List,它仍然是 Fruit 的 List。Apple 的 List 在类型上不等价于 Fruit 的 List,即使 Apple 是一种 Fruit 类型。
真正的问题是我们在讨论的集合类型,而不是集合持有对象的类型。与数组不同,泛型没有内建的协变类型。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
但是,有时你想在两个类型间建立某种向上转型关系。通配符可以产生这种关系。
import java.util.*;
public class GenericsAndCovariance {
public static void main(String[] args) {
// Wildcards allow covariance:
List<? extends Fruit> flist = new ArrayList<>();
// Compile Error: can't add any type of object:
// flist.add(new Apple());
// flist.add(new Fruit());
// flist.add(new Object());
flist.add(null); // Legal but uninteresting
// We know it returns at least Fruit:
Fruit f = flist.get(0);
}
}
flist 的类型现在是 List
,你可以读作“一个具有任何从 Fruit 继承的类型的列表”。然而,这实际上并不意味着这个 List 将持有任何类型的 Fruit。通配符引用的是明确的类型,因此它意味着“某种 flist 引用没有指定的具体类型”。因此这个被赋值的 List 必须持有诸如 Fruit 或 Apple 这样的指定类型,但是为了向上转型为 Fruit,这个类型是什么没人在意。
List 必须持有一种具体的 Fruit 或 Fruit 的子类型,但是如果你不关心具体的类型是什么,那么你能对这样的 List 做什么呢?如果不知道 List 中持有的对象是什么类型,你怎能保证安全地向其中添加对象呢?就像在 CovariantArrays.java 中向上转型一样,你不能,除非编译器而不是运行时系统可以阻止这种操作的发生。你很快就会发现这个问题。
你可能认为事情开始变得有点走极端了,因为现在你甚至不能向刚刚声明过将持有 Apple 对象的 List 中放入一个 Apple 对象。是的,但编译器并不知道这一点。List
可能合法地指向一个 List
。一旦执行这种类型的向上转型,你就丢失了向其中传递任何对象的能力,甚至传递 Object 也不行。
另一方面,如果你调用了一个返回 Fruit 的方法,则是安全的,因为你知道这个 List 中的任何对象至少具有 Fruit 类型,因此编译器允许这么做。
现在你可能会猜想自己不能去调用任何接受参数的方法,但是考虑下面的代码:
import java.util.*;
public class CompilerIntelligence {
public static void main(String[] args) {
List<? extends Fruit> flist = Arrays.asList(new Apple());
Apple a = (Apple) flist.get(0); // No warning
flist.contains(new Apple()); // Argument is 'Object'
flist.indexOf(new Apple()); // Argument is 'Object'
}
}
这里对 contains()
和 indexOf()
的调用接受 Apple 对象作为参数,执行没问题。这是否意味着编译器实际上会检查代码,以查看是否有某个特定的方法修改了它的对象?
通过查看 ArrayList 的文档,我们发现编译器没有那么聪明。尽管 add()
接受一个泛型参数类型的参数,但 contains()
和 indexOf()
接受的参数类型是 Object。因此当你指定一个 ArrayList
时,add()
的参数就变成了"? extends Fruit"。从这个描述中,编译器无法得知这里需要 Fruit 的哪个具体子类型,因此它不会接受任何类型的 Fruit。如果你先把 Apple 向上转型为 Fruit,也没有关系——编译器仅仅会拒绝调用像 add()
这样参数列表中涉及通配符的方法。
contains()
和 indexOf()
的参数类型是 Object,不涉及通配符,所以编译器允许调用它们。这意味着将由泛型类的设计者来决定哪些调用是“安全的”,并使用 Object 类作为它们的参数类型。为了禁止对类型中使用了通配符的方法调用,需要在参数列表中使用类型参数。
下面展示一个简单的 Holder 类:
import java.util.Objects;
public class Holder<T> {
private T value;
public Holder() {
}
public Holder(T val) {
value = val;
}
public void set(T val) {
value = val;
}
public T get() {
return value;
}
@Override
public boolean equals(Object o) {
return o instanceof Holder && Objects.equals(value, ((Holder) o).value);
}
@Override
public int hashCode() {
return Objects.hashCode(value);
}
public static void main(String[] args) {
Holder<Apple> apple = new Holder<>(new Apple());
Apple d = apple.get();
apple.set(d);
// Holder fruit = apple; // Cannot upcast
Holder<? extends Fruit> fruit = apple; // OK
Fruit p = fruit.get();
d = (Apple) fruit.get();
try {
Orange c = (Orange) fruit.get(); // No warning
} catch (Exception e) {
System.out.println(e);
}
// fruit.set(new Apple()); // Cannot call set()
// fruit.set(new Fruit()); // Cannot call set()
System.out.println(fruit.equals(d)); // OK
}
}
Holder 有一个接受 T 类型对象的 set()
方法,一个返回 T 对象的 get()
方法和一个接受 Object 对象的 equals()
方法。正如你所见,如果创建了一个 Holder
,就不能将其向上转型为 Holder
,但是可以向上转型为 Holder
。如果调用 get()
,只能返回一个 Fruit——这就是在给定“任何扩展自 Fruit 的对象”这一边界后,它所能知道的一切了。
如果你知道更多的信息,就可以将其转型到某种具体的 Fruit 而不会导致任何警告,但是存在得到 ClassCastException 的风险。set()
方法不能工作在 Apple 和 Fruit 上,因为 set()
的参数也是"? extends Fruit",意味着它可以是任何事物,编译器无法验证“任何事物”的类型安全性。
但是,equals()
方法可以正常工作,因为它接受的参数是 Object 而不是 T 类型。因此,编译器只关注传递进来和要返回的对象类型。它不会分析代码,以查看是否执行了任何实际的写入和读取操作。
Java 7 引入了 java.util.Objects 库,使创建 equals()
和 hashCode()
方法变得更加容易,当然还有很多其他功能。
还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass>
,或者甚至使用类型参数: <?super T>
(尽管你不能对泛型参数给出一个超类型边界;即不能声明
)。这使得你可以安全地传递一个类型对象到泛型类型中。因此,有了超类型通配符,就可以向 Collection 写入了:
import java.util.*;
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Fruit()); // Error
}
}
参数 apples 是 Apple 的某种基类型的 List,这样你就知道向其中添加 Apple 或 Apple 的子类型是安全的。但是因为 Apple 是下界,所以你知道向这样的 List 中添加 Fruit 是不安全的,因为这将使这个 List 敞开口子,从而可以向其中添加非 Apple 类型的对象,而这是违反静态类型安全的。
下面的示例复习了一下逆变和通配符的的使用:
import java.util.*;
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruit = Arrays.asList(new Fruit());
static <T> T readExact(List<T> list) {
return list.get(0);
}
// A static method adapts to each call:
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruit);
f = readExact(apples);
}
// A class type is established
// when the class is instantiated:
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruit);
//- Fruit a = fruitReader.readExact(apples);
// error: incompatible types: List
// cannot be converted to List
}
static class CovariantReader<T> {
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader = new CovariantReader<>();
Fruit f = fruitReader.readCovariant(fruit);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();
f2();
f3();
}
}
readExact()
方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就可以向 List 中写入和读取这个精确类型。另外,对于返回值,静态的泛型方法 readExact()
可以有效地“适应”每个方法调用,并能够从 List
中返回一个 Apple ,从 List
中返回一个 Fruit ,就像在 f1()
中看到的那样。因此,如果可以摆脱静态泛型方法,那么在读取时就不需要协变类型了。
然而对于泛型类来说,当你创建这个类的实例时,就要为这个类确定参数。就像在 f2()
中看到的,fruitReader 实例可以从 List
中读取一个 Fruit ,因为这就是它的确切类型。但是 List
也应该产生 Fruit 对象,而 fruitReader 不允许这么做。
为了修正这个问题,CovariantReader.readCovariant()
方法将接受 List<?extends T>
,因此,从这个列表中读取一个 T 是安全的(你知道在这个列表中的所有对象至少是一个 T ,并且可能是从 T 导出的某种对象)。在 f3()
中,你可以看到现在可以从 List
中读取 Fruit 了。
无界通配符 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:
import java.util.*;
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
//- list3 = list;
// warning: [unchecked] unchecked conversion
// list3 = list;
// ^
// required: List
// found: List
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
//- assign3(new ArrayList());
// warning: [unchecked] unchecked method invocation:
// method assign3 in class UnboundedWildcards1
// is applied to given types
// assign3(new ArrayList());
// ^
// required: List
// found: ArrayList
// warning: [unchecked] unchecked conversion
// assign3(new ArrayList());
// ^
// required: List
// found: ArrayList
// 2 warnings
assign1(new ArrayList<>());
assign2(new ArrayList<>());
assign3(new ArrayList<>());
// Both forms are acceptable as List:
List<?> wildList = new ArrayList();
wildList = new ArrayList<>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是原生类型还是 。在这些情况中,
可以被认为是一种装饰,但是它仍旧是很有价值的,因为,实际上它是在声明:“我是想用 Java 的泛型来编写这段代码,我在这里并不是要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。”
第二个示例展示了无界通配符的一个重要应用。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要:
import java.util.*;
public class UnboundedWildcards2 {
static Map map1;
static Map<?, ?> map2;
static Map<String, ?> map3;
static void assign1(Map map) {
map1 = map;
}
static void assign2(Map<?, ?> map) {
map2 = map;
}
static void assign3(Map<String, ?> map) {
map3 = map;
}
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
//- assign3(new HashMap());
// warning: [unchecked] unchecked method invocation:
// method assign3 in class UnboundedWildcards2
// is applied to given types
// assign3(new HashMap());
// ^
// required: Map
// found: HashMap
// warning: [unchecked] unchecked conversion
// assign3(new HashMap());
// ^
// required: Map
// found: HashMap
// 2 warnings
assign1(new HashMap<>());
assign2(new HashMap<>());
assign3(new HashMap<>());
}
}
但是,当你拥有的全都是无界通配符时,就像在 Map
中看到的那样,编译器看起来就无法将其与原生 Map 区分开了。另外, UnboundedWildcards1.java 展示了编译器处理 List
和 List
是不同的。
令人困惑的是,编译器并非总是关注像 List
和 List
之间的这种差异,因此它们看起来就像是相同的事物。事实上,因为泛型参数擦除到它的第一个边界,因此 List
看起来等价于 List