0%

Java 泛型通配符和类型限定

1. 概述

对于泛型系统而言,最重要的就是能够领过的使用;

所以,Java 增加了 类型通配符(?)

使用 ? 来表示 任何类型

例如:

1
Pair<? extends Employee>

表示 任何Empolyee 的子类;

2. 与类型参数 T 的不同

类型参数 T 和 通配符 ? 似乎都是用于实现泛型灵活性的工具;

但是,实际上它们有着很大的不同

2.1 使用位置不同

在泛型中,类型参数 T 是一个 确定的 类型,

通常用于泛型类和泛型方法的 定义,不能用于调用代码。

1
2
public static <T extends Number> void
copy(List<T> dest, List<T> src)

而通配符是一个 不确定 的类型,通常用于泛型方法的 调用代码 和形参,不能用于定义类和泛型方法

1
Pair<? Empolyee> wildcardBuddies = managerBuddies;

2.2 类型参数保证形参一致性

基于 T 是一个确定的类型,那么就可以通过 T确保 泛型参数的一致性;

例如上面的 copy() 方法,使用泛型参数 T确保了 两个 List 的元素类型是 一致的

但是,如果使用如下的代码:

1
2
public static void
copy(List<? extends Number> dest, List<? extends Number> src)

由于通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型

2.3 类型参数可以多重限定而通配符不行

类型参数 T 可以进行多重限定,如:

1
T extends A & B & C

但是通配符不能进行多重限定,因为它不是一个确定的类型。

2.4 通配符可以使用超类限定而类型参数不行

类型参数 T 只具有 一种 类型限定方式:

1
T extends A

但是通配符 ? 可以进行 两种限定

1
2
? extends A
? super A

3. 通配符类型限定和继承

对于泛型来说,即使它的类型参数具有继承关系,泛型类之间也是 没有 任何关联的;

但是,通过通配符的限定,就让泛型类之间具有了公有的父类;

例如:

Pair<? extends Empolyee>Pair<Manager>Pair<Empolyee>公共超类

事实上, Pair<? extends Empolyee>Pair<Empolyee> 和所有以 Empolyee 子类 为类型参数的泛型类的超类。

对于超类型限定,同理;

1
? super Empolyee

表示类型参数可能是 Empolyee某个超类

和子类型限定同理,超类型限定也可以为泛型类建立联系:

如上,Pair<? super Manager>Pair<Empolyee>Pair<Object> 的共同超类。

需要注意的是,在 Pair<? super Manager> 之上,还有一个 无限定通配符,指代任何的类型。

4. PECS 原则

PECS 原则,指的就是 Producer Extends Consumer Super

意思就是,对于生产者,采用 ? extends

对于消费者,采用 ? super

对于既可能是生产者有可能是消费者的对象,则 不做类型限定

首先说明所谓的生产者和消费者的主语都是 数据结构

例如:

1
List<? extends Number> producer;

那么,List 就是 生产者,提供数据,调用 get() 方法,外部使用者提取其内部数据。

反过来,对于

1
List<? super Number> consumer;

那么此时,List 就是 消费者,接收数据,调用 add() 方法,外部使用者将数据注入结构中。

实际上,这个原则是为了能够在泛型中提供尽可能的类型安全的写法。

4.1 生产者的读方法

1
List<? extends Number> numbers;

这个就是很典型的生产者;

由于使用了上界限定,那么它 保证 其中的元素 一定Number

所以它可以调用 get() 方法;

4.2 生产者的写方法

但是此时,生产者不可以使用写方法;

这是因为 ? extends Number 只是限定了上界;

换句话说也就是 Number 的某个 子类型

由于不知道具体是 哪个 子类型,所以,也就无法对其进行写入。

那么是否可以写入 Number 对象呢?

很可惜,这也是不可以的,因为 Number 是上界,很可能由于多态的原因变成了它的某个子类;

所以对于 ? extends 来说,禁止所有的写入操作。

4.3 消费者的读方法

根据原则,具有下面类型的是消费者:

1
List<? super Number> numbers;

对于消费者来说,它接受的是 Number 的某个超类型;

那么,由于不清楚是 哪个 超类型;

所以,实际上返回的是最终的 Object 对象;

这显然对于读取没有什么帮助,如果读到一个 Object 类型,用户还需要将其转化为对应的子类型;

这显然是多此一举的。

注意,消费者 并非是禁止 读操作的访问

4.4 消费者的写操作

那么对于写操作如何呢?

1
List<? super Integer> numbers = new ArrayList<Integer>;

此时,对于 numbers 来说,可以使用 add() 方法加入 Integer

但是,能否加入 Number 元素呢?(NumberInteger 的超类);

很遗憾,这是 不行的!

numbers 目前只能写入 Integer 及其子类;

因为 Integer 的子类也是 Integer ,数据结构将子类当成 Integer 来处理

实际上,你 无法加入任何的超类!

如果允许加入 Number 甚至是 Object,那会造成什么结果呢?

就是一个 Integer 的列表中混入了 Object

显然这是不合理的。

那么为什么有必要使用 ? super 这种多此一举的声明呢?

从超类限定的关系图中我们可以看出,它在泛型类关系中,处于 超类 的地位;

所以,我们就可以通过它所提供的 多态,来将我们目前存储的数据,加入到一个 更抽象通用的 数据结构中。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void count(Collection<? super Integer> ints, int n)
{
for (int i = 0; i < n; i++) ints.add(i);
}

public static void main(String[] args) {
List<Integer>ints = new ArrayList<>();
count(ints, 5);
assert ints.toString().equals("[0, 1, 2, 3, 4]");

List<Number>nums = new ArrayList<>();
count(nums, 5); nums.add(5.0);
assert nums.toString().equals("[0, 1, 2, 3, 4, 5.0]");

List<Object>objs = new ArrayList<>();
count(objs, 5); objs.add("five");
assert objs.toString().equals("[0, 1, 2, 3, 4, five]");
}

虽然我们的 count() 方法 只能 提供 Integer 数据;

但是,由于使用了 <? super Integer>,所以我们的数据可以被放入到 List<Number>List<Object> 中。

? super 提供的 多态支持,正是我们采用 Consumer Super 的关键。

4.5 不做限定

当一个类既可能是生产者,又可能是消费者时;

就不对它的类型作出限定。

注意这里所说的 不是 无限定的通配符,而是指的是普通的类型参数形式(T)

1
public class Stack<T>{}

5. 无限定通配符

有时候我们会使用无限定的通配符。

例如:

1
Pair<?>

此时,它的 get() 返回值类型只能是 Object

同时, 不能 调用 set(),即使通过 Object 也不能调用。

那么这时有什么用处呢?

我们可以用它实现一些与类型 无关 的操作;

1
2
3
public static boolean hasNulls(Pair<?> piar) {
return p.getFirst() == null || p.getSecond() == null;
}

因为 hasNulls() 方法不需要关心类型是什么;

这个时候我们就可以使用无限定的通配符。

6. 通配符捕获

有时候,当我们需要实现一个无限定通配符方法的时候;

有可能遇到需要使用类型参数的问题。

例如:

1
public static void swap(Pair<?> pair)

但是,由于 ? 不是类型参数,所以我们需要使用一个辅助方法来实现。

1
2
3
4
5
public static <T> void swapHelper(Pair<T> pair) {
T t = pair.getFirst();
pair.setFirst(p.getSecond());
pair.setSecond(t);
}

那么我们就可以通过下面的方法实现 swap() 方法:

1
2
3
public static void swap(Pair<?> pair) {
swapHelper(pair);
}

但是,对于这个例子来说,实现一个辅助方法的确是多此一举;

我们本来就可以通过辅助方法直接实现需求;

那么为什么需要这个技巧呢?

在使用了通配符的时候,不可避免的需要使用到这个技巧:

1
2
3
4
5
6
public static void
maxminBonus(Manager[] a, Pair<? super Manager> result)
{
minmaxBonus(a, result);
PairAlg.swap(result);
}

由于使用了通配符,那么这个捕获技巧就不可避免的需要了。