泛型总结之通配符

一、介绍通配符之前

  首先在引出通配符之前先看看数组的一种特殊行为:可以向导出类型的数组赋予基类型的数组引用,先看看代码:

package com.jxs.chapeter15;

class Fruit{}

class Apple extends Fruit {}

class Jonathan extends Apple {}

class Orange extends Fruit {}

public class CovarianArrays {

    public static void main(String[] args) {

        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();

        try {
            fruit[0] = new Fruit();
        } catch (Exception e) {
            System.out.println(e);
        }
        try {
            fruit[0] = new Orange();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

运行结果:

java.lang.ArrayStoreException: com.jxs.chapeter15.Fruit
java.lang.ArrayStoreException: com.jxs.chapeter15.Orange

Process finished with exit code 0

  在上面的代码可以看到,main() 中的第一行创建了一个 Apple 数组,并将其赋值给了一个 Fruit 数组引用。这是有意义的,因为Apple 是 Fruit 的子类,一个 Apple 对象也是一种 Fruit 对象,所以一个 Apple 数组也是一种 Fruit 的数组。尽管 Apple[] 可以“向上转型”为 Fruit[],但数组元素的实际类型还是 Apple,我们只能向数组中放入 Apple 或者 Apple 的子类。但由于创建的 Apple 数组有 Fruit[] 引用,导致它可以将 Fruit 对象或者任何从 Fruit 继承出来的对象放置到这个数组当中。所以在编译时并不会出现错误。所以对编译器来说,向数组中放入 Fruit 对象和 Orange 对象是可以通过编译的。但是在运行时期,数组的实际类型是 Apple[] ,JVM 发现了你插入了不正确的类型,所以当其它对象加入数组的时候就会抛出异常。
  我们都知道泛型的主要目标之一就是为了将上述的这种运行时错误检测移入到编译期,假如说我们使用泛型容器代替数组的时候会出现什么情况呢,看看下面这段代码:

public class NonCovariantGenerics {

    // Compile Error: imcompatible types:
    List flist = new ArrayList();
}

  什么情况?编译失败了?其实你仔细观察就会发现尽管 Apple 是 Fruit 的子类,但是 ArrayList 不能说是 ArrayList 的子类,也就不存在说 ArrayList 是 ArrayList 的子类了,他们只是在限定了装入集合的类型。
  但是问题来了,现在你要想在两个类型之间建立某种类型的向上转型关系该怎么办呢?这个时候就出现了通配符这个神器。通配符可以分为上边界限定通配符,下边界限定通配符和无边界通配符,下面就来一一进行相关的介绍。

二、上边界限定通配符

上边界限定通配符的表示形式:? extends T

1.使用上边界限定通配符出现的问题

于是利用上边界限定通配符实现泛型的向上转型:
GenericsAndCovariance.java

package com.jxs.chapeter15;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by jiangxs on 2018/4/11.
 */
public class GenericsAndCovariance {

    public static void main(String[] args) {

        // Wildcards allow covariance:
        List flist = new ArrayList();
        // Compile Error: can't add any type of object:
        // flist.add(new Apple());  error
        // flist.add(new Fruit());   error
        // flist.add(new Object());   error
        flist.add(null);  // legal but uninteresting
        // we know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
}

  为什么这个集合flist除了能添加无意义的 null 以外什么都添加不了呢?看了《Java编程思想》中的解释后才明白集合 List,表面上来看意思是:具有任何从Fruit继承的类型的列表,于是就很容易错误的理解为这个集合中可以存放 Fruit 以及 Fruit 的子类。实际上通配符引用的是明确的类型,它意味着表示一个特定的类型,而 flist 这个集合并没有去指定这个具体的类型。我们只知道这个类型是 Fruit 的子类型,而 Fruit 是它的上边界。也就是说 flist 应该指向某个类型的 List,只要这个类型是 Fruit 或者 Fruit 的某个子类就可以了。
  但是上面的代码中 List 表示某种特定类型 ( Fruit 或者Fruit的子类 ) 的 List,我们并不知道这个实际的类型到底是什么,只知道是 Fruit 或者 Fruit 的子类。但是这也并不是意味着实际类型只要是 Fruit 或者 Fruit 的子类就可以,实际类型必须要被明确。所以编译器并不能放心的去安全的添加一个对象,所以在向 flist 中添加任何对象时,无论是 Apple 还是 Orange 甚至是 Fruit 对象都会编译失败
  另一方面,如果调用一个返回 Fruit 的方法,则是安全的。因为在这个 List 中无论它的实际类型是什么,总可以将它转型为 Fruit,所以编译器允许返回 Fruit。

2.没得玩了?

  从GenericsAndCovariance.java中我们可以看到 flist 的 add 操作什么对象都无法添加,是不是其他的操作也不可以了呢?下面就来试试:

package com.jxs.chapeter15;

import java.util.Arrays;
import java.util.List;

/**
 * Created by jiangxs on 2018/4/12.
 */
public class CompilerIntelligence {

    public static void main(String[] args) {

        List flist = Arrays.asList(new Apple());
        Apple apple = (Apple) flist.get(0); // No Warning
        flist.contains(new Apple());// Argument is 'Object'
        flist.indexOf(new Apple());// Argument is 'Object'
    }
}

  发现这些好像都可以编译通过并正常运行,但是为什么GenericsAndCovariance.java中 flist 的 add 操作除了添加 null 以外什么都不能添加呢?按道理来说泛型参数都使用了受限制的通配符,为什么会出现有的操作可以有的操作不行呢?于是我们来看看 add() 方法,contains 方法以及 indexOf 方法的源码把:
List.java

boolean add(E e);
boolean contains(Object o);
int indexOf(Object o);

  看了一下这些方法的接口之后似乎结果就明朗了,假如我们指定泛型的类型为的时候,由于add()方法中接受的参数类型变为了 ? extends Fruit ,此时编译器并不知道这个参数是Fruit类的哪个子类,所以为了保证安全性,它不接受任何类型了。
但是contains()方法和indexOf()方法与add()方法不同:他们接收的参数类型是Object类,它们没有涉及到通配符的问题。所以编译期调用它们也不会出现之前的问题。这意味着将由泛型类的设计者决定哪些调用是“安全的”,并使用Object类型作为其参数类型。为了在类型中使用通配符的情况下禁止这种类型参数是通配符的调用,我们需要在参数列表中使用类型参数,比如List.java中的add(E e)方法。
我们可以在下面的一个例子中看到这点:
Holder.java

package com.jxs.chapeter15;

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;
    }

    public boolean equals(Object obj) {

        return value.equals(obj);
    }

    public static void main(String[] args) {

        Holder apple = new Holder<>(new Apple());
        Apple d = apple.get();
        apple.set(d);

        // Holder fruit = apple;   Can no Upcast
        Holder fruit = apple;   //ok
        Fruit p = fruit.get();
        d = (Apple) fruit.get();   // Returns 'Object'
        try {
            Orange c = (Orange) fruit.get();
        } catch (Exception e) {
            System.out.println(e);
        }
        // fruit.set(new Apple());
        // fruit.set(new Fruit());
        System.out.println(fruit.equals(d));   // ok
    }
}

运行结果:

java.lang.ClassCastException: com.jxs.chapeter15.Apple cannot be cast to com.jxs.chapeter15.Orange
true

Process finished with exit code 0

  Holder有一个接收T类型对象的set()方法,一个返回类型为T的get()方法以及一个接收Object对象的equals()方法。我们在上面的代码中可以看到,如果创建了一个Holder,不能将其向上转型为Holder,但是可以将它向上转型为Holder。
如果调用get()方法,它只会返回一个Fruit类型,因为? extends Fruit相当于给定了“任何扩展自Fruit对象”这个边界,它相当于只确定了Fruit类型这个边界类型肯定是没有问题的,所以它只会返回一个Fruit类型。
而set()方法不会接受任何类型的添加,因为set()方法的接受参数是? extends Fruit的,这意味着它可以是任何事物,而编译器无法验证“任何事物”类型的安全性。
  但是equals()方法却可以正常的工作,因为它接受Object类型的不是T类型的参数。因此编译器只会关注传递进来的和要返回的对象类型,它并不会分析代码以查看是否执行了任何实际的读入和读取操作。

三、下边界限定通配符

通配符还有一种形式叫做下边界限定通配符,也叫做超类型通配符。
下边界限定通配符的表示形式:? super T
  
  其中T是类型参数的下界(所有的都是T的super),泛型参数不能接收超过边界的类型。这样你就可以安全的传递一个T或者T的子类型对象到泛型类型中。使用下边界限定通配符就可以解决之前遇到的问题了:

package com.jxs.chapeter15;

import java.util.List;

/**
 * Created by jiangxs on 2018/4/12.
 */
public class SuperTypeWildcards {

    static void writeTo(Listsuper Apple> apples) {

        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit());   // Error
    }
}

  writeTo 方法的接收参数 apples 的类型是 List,它表示某种类型的 List,这个类型是 Apple 的父类型。也就是说,我们不知道实际类型是什么,但是这个类型肯定是 Apple 的父类型。所以说向这个List中无法添加Apple类的父类,因为不确定实际的类型是什么。但是,向这个 List 添加一个 Apple 或者其子类型的对象是安全的,因为这些对象都可以向上转型为 Apple。apples.add(new Fruit())编译失败是因为Fruit类为Apple类的父类,而传入参数类型超过了下边界后的类型我们并不能确定,所以我们不知道加入 Fruit 对象是否安全,那样就会使得这个 List 添加了与 Apple 无关的类型。所以会出现编译失败的情况。
  在了解了子类型边界和超类型边界之后,我们就可以知道如何向泛型类型中 “写入” ( 传递对象给方法参数) 以及如何从泛型类型中 “读取” ( 从方法中返回对象 )。下面是一个例子:

public class Collections { 
  public static  void copy(Listsuper T> dest, List src) 
  {
      for (int i=0; i

  src 是原始数据的 List,因为要从这里面读取数据,所以用了上边界限定通配符:,取出的元素转型为 T。dest 是要写入的目标 List,所以用了下边界限定通配符:,可以写入的元素类型是 T 及其子类型。
  好了,接下来就是用下边界限定通配符来完成将Apple放置到List中:

package com.jxs.chapeter15;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by jiangxs on 2018/4/12.
 */
public class GenericWriting {

    static  void writeExact(List list, T item) {

        list.add(item);
    }

    static List apples = new ArrayList<>();
    static List fruit = new ArrayList<>();

    static void f1() {

        writeExact(apples, new Apple());
        try {
            writeExact(fruit, new Apple());
        } catch (Exception e) {
            System.out.println(e);
        }
    }

    static  void writeWithWildcard(Listsuper T> list, T item) {

        list.add(item);
    }

    static void f2() {

        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
    }

    public static void main(String[] args) {

        f1();
        f2();
    }
}

打扰一下~
在研究之前先说一下《Java编程思想》在这段代码中的一点小问题:我们发现在原书中的这段代码的f1()方法中的

writeWithWildcard(fruit, new Apple());

这个语句是会报出一个Error的:

Error: Incompatible types: found Fruit, required Apple

但是现在LZ在调试的时候写了这个语句后再编译和运行时都没有相关的错误产生。后来查阅了相关资料后发现网上的说法是这样的:
事实上,这里匹配时如果T识别为Fruit,是没有问题的。如果T识别为Apple,则会发生错误。可用显式泛型方法调用验证:

GenericWriting.(fruit, new Apple());

这样写就不会有错,但是

GenericWriting.(fruit, new Apple());

这样写则会报错。估计JDK8把T识别为Fruit,而JDK1.5识别为Apple。
好了,言归正传~
  在writeWithWildcard()方法中,它的参数现在List

四、无边界通配符

  无边界通配符的表示形式:
  无边界通配符看起来好像代表着任何类型,因此使用无边界通配符好像等价于原生类型。既然这样List

package com.jxs.chapeter15;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by jiangxs on 2018/4/12.
 */
public class UnboundedWildcards {

    public static void main(String[] args) {

        List list1 = new ArrayList();
        List list2 = new ArrayList<>();
        list1.add(1);
        // list2.add(1); // Error add (capture)in List cannot be applied to (int)
    }
}

  为什么List和List好像是一样的,list1却可以使用add()方法添加而list2却不可以呢?
  因为List list1 表示 list1 是持有某种特定类型的 List,但是不确定具体是哪种类型。于是就像前面说的那样,为了安全起见,不能向其中添加对象。而 List list ,也就是没有传入泛型参数,表示这个 list 持有的元素的类型是 Object,因此可以添加任何类型的对象。

五、总结一下

  通配符主要作用是可以对泛型参数做出某些限制,使代码更安全,对于上边界和下边界限定的通配符总结如下:

1.上边界限定的通配符

  使用 List list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的子类 ) 的对象,这个对象包含的元素类型是 C 的子类型 ( 包含 C 本身)的一种。
例如:

List list = new ArrayList();

  list不能使用接收泛型参数的方法,例如之前说过的add(E e);

2.下边界限定的通配符

  使用 List list 这种形式,表示 list 可以引用一个 ArrayList ( 或者其它 List 的子类 ) 的对象,这个对象包含的元素就类型是 C 的超类型 ( 包含 C 本身 ) 的一种。
例如:

Listsuper Apple> list = new ArrayList();

  list可以使用接收泛型的方法,传入的泛型的类型必须为T类型或者T类型的子类型。

3.无边界通配符

  无边界通配符可以代表任何事物,但是使用无边界通配符并不等价于原生类型,例如之前举例的List和List。

参考:
《Java编程思想》

https://segmentfault.com/a/1190000005337789utm_source=tuicool&utm_medium=referral

https://book.douban.com/review/7803218/

你可能感兴趣的:(javaSE,泛型,协变,逆变,通配符,java)