Java泛型中的通配符、递归边界与类型擦除
立即解锁
发布时间: 2025-08-17 02:35:38 阅读量: 15 订阅数: 22 


Java编程基础与SCJP认证指南
### Java泛型中的通配符、递归边界与类型擦除
#### 1. 捕获转换(Capture Conversion)
在Java编程中,有时候我们会遇到一些方法在使用泛型时出现编译错误的情况。比如下面这个非泛型方法:
```java
static void fillWithFirstV1(List<?> list) {
Object firstElement = list.get(0); // (1)
for (int i = 1; i < list.size(); i++)
list.set(i, firstElement);
// (2) Compile-time error
}
```
这个方法的目的是将传入列表的第一个元素填充到列表的其他位置。然而,`set` 方法调用会产生编译错误,因为使用 `<?>` 引用时,`set` 操作是不允许的。这表明使用通配符 `?` 来参数化列表是行不通的。
我们可以使用泛型方法的类型参数来替代通配符,如下所示:
```java
static <E> void fillWithFirstOne(List<E> list) {
E firstElement = list.get(0);
// (3)
for (int i = 1; i < list.size(); i++)
list.set(i, firstElement);
// (4)
}
```
由于参数类型是 `List<E>`,我们可以从列表中获取和设置类型为 `E` 的对象。同时,我们将 `firstElement` 的类型从 `Object` 改为 `E`,以便设置列表的第一个元素。
如果重新实现 `fillWithFirstV1` 方法,调用泛型方法 `fillWithFirstOne`,代码就能正常工作:
```java
static void fillWithFirstV2(List<?> list) {
fillWithFirstOne(list);
// (5) Type conversion
}
```
`fillWithFirstV1` 方法参数中的通配符有一个类型捕获。在调用 `fillWithFirstOne` 方法时,这个类型捕获会转换为类型 `E`,这种转换称为捕获转换。捕获转换在某些特定条件下会起作用,但具体条件超出了本文的讨论范围。
#### 2. 嵌套通配符(Nested Wildcards)
对于无界类型参数 `<T>`,子类型关系是不变的。例如:
```java
Collection<Number> colNum;
Set<Number> setNum;
Set<Integer> setInt;
colNum = setNum; // (1) Set<Number> <: Collection<Number>
colNum = setInt; // (2) Compile-time error!
```
当使用具体的参数化类型作为实际类型参数,即嵌套参数化类型时,情况也是一样的:
```java
Collection<Collection<Number>> colColNum; // Collection of Collections of Number
Set<Collection<Number>> setColNum; // Set of Collections of Number
Set<Set<Integer>> setSetInt; // Set of Sets of Integer
colColNum = setColNum;
// (3) Set<Collection<Number>> <: Collection<Collection<Number>>
colColNum = setSetInt;
// (4) Compile-time error!
setColNum = setSetInt;
// (5) Compile-time error!
```
我们可以使用上界通配符来引入子类型协变。以下代码中,上界通配符应用在顶层:
```java
Collection<? extends Collection<Number>> colExtColNum;
colExtColNum = colColNum;
// (6) Collection<Collection<Number>> <: Collection<? extends Collection<Number>>
colExtColNum = setColNum;
// (7) Set<Collection<Number>> <: Collection<? extends Collection<Number>>
colExtColNum = setSetInt;
// (8) Compile-time error!
```
如果将通配符应用在最内层:
```java
Collection<Collection<? extends Number>> colColExtNum;
colColExtNum = colColNum;
// (9) Compile-time error!
colColExtNum = setColNum;
// (10) Compile-time error!
colColExtNum = setSetInt;
// (11) Compile-time error!
```
上述赋值操作表明,上界通配符仅在顶层引入子类型协变。在 (9) 中,类型 `A`(`Collection<Number>`)是类型 `B`(`Collection<? extends Number>`)的子类型,但由于参数化类型之间不存在子类型协变关系,`Collection<A>`(`Collection<Collection<Number>>`)不是 `Collection<B>`(`Collection<Collection<? extends Number>>`)的子类型。
当参数化类型有多个类型参数时,情况也是类似的:
```java
Map<Number, String> mapNumStr;
Map<Integer, String> mapIntStr;
mapNumStr = mapIntStr;
// (12) Compile-time error!
```
同样,上界通配符只能在顶层使用以引入子类型协变:
```java
Map<Integer, ? extends Collection<String>> mapIntExtColStr;
Map<Integer, Collection<? extends String>> mapIntColExtStr;
Map<Integer, Collection<String>> mapIntColStr;
Map<Integer, Set<String>> mapIntSetStr;
mapIntExtColStr = mapIntColStr;
// (13) Map<Integer, Collection<String>> <: Map<Integer, ? extends Collection<String>>
mapIntExtColStr = mapIntSetStr;
// (14) Map<Integer, Set<String>> <: Map<Integer, ? extends Collection<String>>
mapIntColStr = mapIntSetStr;
// (15) Compile-time error!
mapIntColExtStr = mapIntColStr;
// (16) Compile-time error!
mapIntColExtStr = mapIntSetStr;
// (17) Compile-time error!
```
#### 3. 通配符参数化类型作为形式参数
接下来,我们探讨将通配符参数化类型作为方法形式参数的影响。假设我们要在 `MyStack<E>` 类中添加一个方法,用于将源栈的元素移动到当前栈。以下是三种实现尝试:
```java
public void moveFromV1(MyStack<E> srcStack) {
// (1)
while (!srcStack.isEmpty())
this.push(srcStack.pop());
}
public void moveFromV2(MyStack<? extends E> srcStack) {
// (2)
while (!srcStack.isEmpty())
this.push(srcStack.pop());
}
public void moveFromV3(MyStack<? super E> srcStack) {
// (3) Compile-time error!
while (!srcStack.isEmpty())
this.push(srcStack.pop());
}
```
假设有以下三个栈:
```java
MyStack<Number> numStack = new MyStack<Number>();
// Stack of Number
numStack.push(5.5); numStack.push(10.5); numStack.push(20.5);
MyStack<Integer> intStack1 = new MyStack<Integer>();
// Stack of Integer
intStack1.push(5); intStack1.push(10); intStack1.push(20);
MyStack<Integer> intStack2 = new MyStack<Integer>();
// Stack of Integer
intStack2.push(15); intStack2.push(25); intStack2.push(35);
```
使用 `moveFromV1` 方法,我们只能在相同类型的栈之间移动元素:
```java
intStack1.moveFromV1(intStack2);
numStack.moveFromV1(intStack2);
// Compile-time error!
```
上述编译错误是因为 `MyStack<Integer>` 不是 `MyStack<Number>` 的子类型。然而,使用 `moveFromV2` 方法,我们可以将 `MyStack<? extends E>` 类型栈的元素移动到当前栈。这是因为 `MyStack<? extends E>` 类型的引用可以指向包含类型 `E` 或其子类对象的栈,并且 `pop` 操作是允许的,返回的对象实际类型受上界 `E` 限制,该对象可以放入类型为 `E` 或其超类型的栈中。
```java
intStack1.moveFromV2(intStack2);
numStack.moveFromV2(intStack2);
```
`moveFromV3` 方法只允许从 `MyStack<? super E>` 类型的栈中弹出 `Object` 类型的对象,这些对象只能压入 `Object` 类型的栈。由于在编译时无法确定 `E` 的类型,因此不允许在当前栈上进行 `push` 操作。在这两个方法中,`moveFromV2` 方法在允许更广泛的调用方面更灵活。
同样,我们可以在 `MyStack<E>` 类中添加一个方法,用于将当前栈的元素移动到目标栈:
```java
public void moveToV1(MyStack<E> dstStack) {
// (3)
while (!this.isEmpty())
dstStack.push(this.pop());
}
public void moveToV2(MyStack<? extends E> dstStack) {
// (4)
while (!this.isEmpty())
dstStack.push(this.pop());
// Compile-time error!
}
public void moveToV3(MyStack<? super E> dstStack) {
// (5)
while (!this.isEmpty())
dstStack.push(this.pop());
}
```
在 `moveToV2` 方法中,`MyStack<? extends E>` 类型的引用不允许在目标栈上进行任何设置操作(这里是 `push` 方法)。`moveToV3` 方法提供了最灵活的解决方案,因为 `MyStack<? super E>` 类型的引用允许对类型为 `E` 或其子类型的对象进行设置操作:
```java
intStack1.moveToV1(intStack2);
intStack1.moveToV1(numStack);
// Compile-time error!
intStack1.moveToV3(intStack2);
intStack1.moveToV3(numStack);
```
基于上述讨论,我们可以编写一个泛型方法,用于将元素从源栈移动到目标栈。以下方法签名是更优的选择,其中可以从源栈弹出类型为 `E` 或其子类型的对象,并将其压入类型为 `E` 或其超类型的目标栈:
```java
public static <T> void move(MyStack<? super T> dstStack, MyStack<? extends T> srcStack) {
while (!srcStack.isEmpty())
dstStack.push(srcStack.pop());
}
// Client code
MyStack.move(intStack2, intStack1);
MyStack.move(numStack, intStack1);
MyStack.move(intStack2, numStack);
// Compile-time error!
```
在方法签名中使用通配符是一种常见的习惯用法,因为上界通配符 `(? extends Type)` 可用于从数据结构中获取对象,下界通配符 `(? super Type)` 可用于在数据结构中设置对象。在方法签名中使用通配符可以提高方法的实用性,特别是在方法调用中指定显式类型参数时。
#### 4. 使用通配符进行灵活比较(Flexible Comparisons with Wildcards)
在泛型编程中,另一个常见的习惯用法是使用 `Comparable<T>` 接口作为边界,并使用下界通配符 `(? super T)` 进行参数化,以在比较时提供更大的灵活性。以下是两个方法声明,都使用 `Comparable<T>` 接口作为边界,但参数化方式不同:
```java
static <T extends Comparable<T>> T max(T obj1, T obj2) { ... } // (1)
static <T extends Comparable<? super T>> T superMax(T obj1, T obj2) { ... } // (2)
```
这两个方法可以用于找出两个可比较对象中的最大值。它们应用于两个不同超类的子类对象。超类 `ProgrammerCMP` 实现了 `Comparable<ProgrammerCMP>` 接口,其子类 `JProgrammerCMP` 和 `CProgrammerCMP` 继承了该接口,这意味着不同子类的对象也可以相互比较。然而,超类 `Programmer` 将 `Comparable<E>` 接口的实现留给了其子类 `JProgrammer` 和 `CProgrammer`,这意味着不同子类的对象不能相互比较。
在使用 `max` 方法时,如果在方法调用中未指定显式类型参数,则不允许比较子类
0
0
复制全文
相关推荐










