好好调戏 Java 范型中的通配符以及边界限定规则

通配符 ? 与 T 的区别

T:作用于模版上,用于将数据类型进行参数化,不能用于实例化对象。
?在实例化对象的时候,不确定泛型参数的具体类型时,可以使用通配符进行对象定义。(有点像 C++ 的 auto、C 语言的 void * 指针)

<T> 等同于 <T extends Object>
<?> 等同于 <? extends Object>

一图看懂Java泛型通配符

<? super MyClass>

set 的角度去看待。

MyClass 是我们知道的提示(一个边界):

  • 不允许调用 get 方法获得 MyClass 的引用(这里是指 MyClass 的 get/set 方法)
    • 因为你不知道用什么去「接收/容纳」这个 ? super MyClass(不能确定上界),只能用 Object
      1
      Object abc = <? super MyClass>.get();
  • 允许调用 set 方法传入 MyClass 的引用
    • 因为 <? super MyClass> 表示的类对象不是 MyClass,就是它的父亲。(作为「左值」)
      1
      <? super MyClass>.set(XXX); // XXX 可以是 MyClass,或者它的子类

当使用 <? super MyClass> 的时候,表明未知类的继承结构处于 Object 和 MyClass 之间,这时编译器只能确定任何返回该未知类型的方法,返回的变量都是 Object 的子类,所以返回的类型就只能确定为 Object,比如 getter 方法。(函数返回的时候,或者赋值给左边的值的时候)

此外,set 的情况还包括使用该未知类型作为参数的方法,该参数一定是 MyClass 或者其父类,所以可以传递 MyClass 及其子类进去。

例子:

1
2
3
4
5
6
7
8
9
public class Collections {
// 把 src 的每个元素复制到 dest 中
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
T t = src.get(i);
dest.add(t);
}
}
} // 这样就可以 copy List<Integer> to List<Number>

<? extends MyClass>

get 的角度去看待。

MyClass 是我们知道的提示(一个边界):

  • 允许调用 get 方法获得 MyClass 的引用
    1
    MyClass clz = <? extends MyClass>.get();  // 因为我们知道 MyClass 是上界
  • 不允许调用 set 方法传入 MyClass 的引用
    1
    <? extends MyClass>.set(XXX);  // 但我们不知道下界是什么,只能取个极限 null(XXX = null)

使用 <? extends MyClass> 的时候,未知类型一定是 MyClass 的子类,但向下延伸到无穷尽,无法判断。所以返回未知类型的方法的返回类型有一个上界,就是 MyClass,即返回类型确定为 MyClass。但是使用未知类型的方法,因为向下继承无限延伸,无法判断下界,所以不能使用该方法。

<?> 通配符

使用 <?> 的时候,可以当作 <? extends Object>,即上界是 Object。

可以使用 get 方法:

1
Object abc = <? extends Object>.get();

不可以使用 set 方法(或者只能 set null):

1
<? extends Object>.set(XXX);  // 无法推断 ? 的下界,所以也无法确定 XXX 的上界。

注意:Pair<?>Pair 不同(不要想当然以为 Pair<?> 可以被放入任何东西!)

  • Pair<?> 相当于 Pair<? extends Object>
  • Pair<Object> 可以调用 set 方法放入任何东西

例子:

1
2
3
4
5
6
public class Pair<T> { ... }
public class PairHelper {
static boolean isNull(Pair<?> p) {
return p.getFirst() == null || p.getLast() == null;
}
}

无限定通配符 <?> 很少使用,可以用 <T> 替换它:

其它快速判断的方法

get-put principle

选择限定通配符时的快速判断方法(get-put principle):

  • Use an extends wildcard when you only get values out of a structure.
  • Use a super wildcard when you only put values into a structure.
  • Don't use a wildcard when you do both.

PECS

PECS(Producer,Extends,Consumer,Super)来源于 Effective Java。

  • Producer(extends)

    这里是生产者的意思。当你要从某个参数中获取某个类型的数据,那么应该声明这个参数类型为 <? extends T>。比如,List<? extends Number> list 表明 list 是一个生产者,你可以从其中取出 Number 对象(或者其实是子类,但是你不一定知道)。
    (等同于 get values)

  • Consumer(super)

    这里是消费者的意思。当某个参数将要消费(使用)到某个类型的数据,那么应该声明这个参数类型为 <? super T>。比如,Collection<? super E> collect 表明 collect 可以消费(使用)或者被放入类型为 E(甚至是子类)的数据。

  • 既要生产又要消费,那就不要使用通配符了。

参考