什么是通配符

通配符类型中, 允许类型参数变化。例如, 通配符类型

Pair<? extends Employee〉

表示任何泛型Pair 类型, 它的类型参数是Employee 的子类。

为什么会存在这种通配符存在呢?比如有一个水果(Fruit)类和苹果(Apple)类,苹果继承于水果,现在有一个盘子,这个盘子按照我们人类的思维来看他可以装水果,那么也可以装苹果

public class Plate<T> {
    private T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

现在如下,我们想要打印这个盘子放了什么东东,我们想要复用这个打印的方法,就会发现不允许,因为我们已经限定了printPlate这个方法入参盘子应该是什么类型的,甚至连他的子类都不允许。

public class GenMain {
    public static void main(String[] args) {
        Plate<Fruit> fruitPlate=new Plate<>();
        printPlate(fruitPlate);
        Plate<Apple> applePlate=new Plate<>();
        printPlate(applePlate);//error
    }
    public static void printPlate(Plate<Fruit> fruitPlate){
        System.out.println(fruitPlate.getItem());
    }
}

Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场。那什么是协变逆变呢?协变就是用一个窄类型替代宽类型,而逆变则用宽类型覆盖窄类型。

我们需要用到通配符Plate<? extends Fruit>来解决我们的问题。

public class GenMain {
    public static void main(String[] args) {
        Plate<Fruit> fruitPlate=new Plate<>();
        Plate<Apple> applePlate=new Plate<>();
        printPlate(fruitPlate);
        printPlate(applePlate);//right
    }
    public static void printPlate(Plate<? extends Fruit> fruitPlate){
        System.out.println(fruitPlate.getItem());
    }
}

通配符和泛型区别

我们学习了通配符和泛型可能就会觉得这两个好像啊,他们有什么区别?首先,类型参数<T>是用于声明一个泛型类或者泛型方法,而<?>是使用泛型类或者泛型方法,也就是说定义泛型之后,我们可能不确定我们会传什么具体的类型,我们可以用一个不确定的类型(也就是通配符)比如我们只知道我们要传递的是Fruit的子类或者自身类型作为参数,那么我们就需要使用通配符。

几种通配符类型

上界通配符(Upper Bounds Wildcards)

例如:Plate<? extends Fruit>,它实现了泛型的协变。也就是说这是一个能放水果以及一切是水果派生类(如Apple)的盘子

如果把Fruit和Apple的例子再扩展一下,食物分成水果和肉类,水果有苹果和香蕉,肉类有猪肉和牛肉,苹果还有两种青苹果和红苹果。

上界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域。

上界通配符
上界通配符

那我们把plate换成List,再看这个例子,

List<? extends Fruit> list=new ArrayList<Apple>( );
list.add(new Apple());//error
list.add(new Banana());//error

我们首先需要知道的是List存放数据的时候只能存放一种类型的数据,所以我们指定了存放Apple,那么为什么我们不能add苹果呢?我们知道Java的泛型是伪泛型,在1.5之前是没有泛型的,Java为了兼容以前的代码,所以泛型的类型会被擦除。所以使用泛型时,为了安全,所有与泛型相关的异常都应该在编译期间发现,因此为了泛型的绝对安全,java在设计时做了相关的限制:在编译之前先检查类型以保证泛型的运行安全,再执行编译。所以编译器是不知道list具体是什么类型的,既然类型被擦除List编译的时候会 是List<Object>,那按道理Object是所有的类的父类,为什么不能往里面add?那我们思考一下泛型的出现是为了什么?就是为了解决类型转换的问题,在没有泛型之前,使用ArrayList需要做强制转换,现在有了泛型很明显这不符合它的设计初衷,所以禁止也是正常。

那为什么我们可以get呢?因为提取数据的时候他的子类很容易隐式转成他的父类类型,这是安全的操作,所以也就是可以get

下界通配符(Lower Bounds Wildcards)

例如:Plate<? super Fruit>,它实现了泛型的逆变。也就是说这是一个能放水果以及一切是水果基类(如Food)的盘子,以及水果父类.对应上面的例子,Plate<? super Fruit>覆盖下图中红色的区域。也就是我们只知道我们到时候会往盘子里装可能是FoodFruit类型的东西。

下界通配符
下界通配符

同理super我们可以往里面set他的子类以及自身类型的数据,因为子类很容易隐性转成父类型,但是不能使用get,因为我们只知道它存入的数据可能是Fruit的任何一个父类型的元素,会涉及类型强制转换的问题,所以为了绝对安全就不允许get

上界下界的长短处

我们引用知乎的一个回答:

<? extend Fruit> 和 <? super Fruit> 是两种箱子。

第一种箱子,Apple <? extend Fruit> appleBox,这种水果箱子只能装苹果,已经装好一些苹果。现在来了一个人,从这个箱子里面拿(get)了一个苹果出来,可以确定的是这个苹果一定是水果,但如果这个人想往里面装(set)葡萄,是不允许的,因为葡萄虽然是水果,但这个箱子只能装苹果。所以,可读不可写。

第二种箱子,Food<? super Fruit> foodBox, 这种食物箱子可以装水果等食物,已经装好了一些食物。现在来了一个人,从这个箱子里面拿(get)了一件食物出来,我们无法确定这个食物一定是水果,但如果这个人想往里面(set)葡萄,是允许的,因为葡萄属于水果,当然也属于食物了。所以,可写不可读。

我觉得解释的十分明晰,也就是PECS原则 (Producer Extends Consumer Super)

无界通配符

还可以使用无限定的通配符, 例如,Pair<?>。初看起来,这好像与原始的Pair 类型一样。实际上, 有很大的不同。类型Pair<?> 有以下方法:

? getFirst()
void setFirst (?)

getFirst 的返回值只能赋给一个ObjectsetFirst 方法不能被调用, 甚至不能用Object 调用。Pair<?> Pair 本质的不同在于: 可以用任意Object 对象调用原始Pair 类的setObject方法。(他可以setFirst(null))

为什么要使用这样脆弱的类型? 它对于许多简单的操作非常有用。例如,下面这个方法
将用来测试一个pair 是否包含一个null引用,它不需要实际的类型。

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

通过将hasNulls 转换成泛型方法,可以避免使用通配符类型:public static <T> boolean hasNulls(Pair<T> p),但是,带有通配符的版本可读性更强。

ListList<?>是否有区别?

List,由于类型擦除的关系,ListList<Object>是等价的,因此List表示的是类型参数是Object的List。

List<?>,表示我这里的类型参数其实是特定的,但是,只是一时没有确定下来。

举个简单的例子:

List<? > list=Lists.newArrayList();
list.add(new Apple());//error

编译报错信息

?编译之后的类型
?编译之后的类型

我们可以关注到,编译的结果并不是使用Object来代替?,而是使用了capture#1来代替,所以你想往里面add显然是add不进去的。

新的疑问

那你到这里大概已经分清楚了? extends T,? super T这两个的用法,但是你可能会提出疑问,那么这两个有什么用处呢,一个能get不能set,一个能set不能get,那么这里就要走出一个误区了,上面的做法仅仅只是作为一个刻意的例子,真正的使用是如开头的例子一样,通配符能够约束传入的类型,以便于我们可以拿到他的一些属性方法,然后做成一个方法族,可以简化大量的重复代码,不然你可能想打印苹果就要再写一个接收入参是Plate<Apple>的方法。

我们以guavaLists类作为例子,这里

List<? extends Fruit> list=Lists.newArrayList(new Banana(),new Apple());
//guava的newArrayList方法
@SafeVarargs
  @GwtCompatible(serializable = true)
  public static <E> ArrayList<E> newArrayList(E... elements) {
    checkNotNull(elements); // for GWT
    // Avoid integer overflow when a large array is passed in
    int capacity = computeArrayListCapacity(elements.length);
    ArrayList<E> list = new ArrayList<>(capacity);
    Collections.addAll(list, elements);
    return list;
  }

这里可以看到newArrayList可以 看到Collections.addAll(list, elements);

public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

addAll方法接收的入参Collection<? super T> ,所以你给给这方法传任意只要是Fruit子类或者他自身的类型的数据,因此你可以往里面丢香蕉、苹果。

参考:https://www.zhihu.com/question/20400700/answer/117464182

http://swiftlet.net/archives/1950

http://swiftlet.net/archives/1950

《Java核心技术卷1》