Thinking In Java Part12(通配符、超类型通配符)

1、通配符
	可以向导出类型的数组赋予基类型的数组引用
	class Fruit{}
	class Apple extends Fruit{}
	class Jonathan extends Apple{}
	class Orange extends Fruit{}
	public class CovariantArrays {
	    public static void main(String[] args) {
	        Fruit[] fruit = new Apple[10];
	        fruit[0] = new Apple();
	        fruit[1] = new Jonathan();
	        try{
	            // java.lang.ArrayStoreException
	            fruit[0] = new Fruit();
	        }catch (Exception e){
	            System.out.println(e);
	        }
	        try{
	            // java.lang.ArrayStoreException
	            fruit[0] = new Orange();
	        }catch (Exception e){
	            System.out.println(e);
	        }
	    }
	}
	创建了一个Apple数组,并将其赋值给一个Fruit数组引用,有意义的,因为Apple也是一种Fruit,因此Apple数组应该也是一个Fruit数组。
	但是,实际的数组类型是Apple[],应该只能放置Apple或apple的子类型,这在编译器和运行时都可以运行。编译器运行你将Fruit放在这个数组中,是因为它有一个Fruit[]引用——因此他运行将Fruit对象或任何从Fruit继承的对象(Orange)放在这个数组中,所以,编译期是运行的,但是,运行时数组机制知道它是Apple[],因此会在向数组中放置异构类型 抛出异常。
	实际中向上转型不合适用在这,我们真正做的是将一个数组赋值给另一个数组。数组的持有其他对象行为是因为向上转型而已,数组本身对自己持有的对象有检测,因此在编译期和运行时检查,我们要小心。
	因为泛型的主要目标之一是将运行时错误移入到编译器,因此我们接下来尝试用泛型容器来代替数组。
		// Compile Error
	    ArrayList<Fruit> apples = new ArrayList<Apple>();
	泛型和容器相关正确的说法为:不能把一个涉及Apple的泛型赋给一个设计Fruit的泛型。如果像在数组的情况中一样,编译器对代码的了解足够多,可以确定所涉及到的容器,那么它可能运行编译时通过,但是它不知道任何有关这方面的信息,因此它拒绝向上转型。实际上着也不是向上转型——Apple的List不是Fruit的List。Apple的List将持有Apple和Apple的子类型,而Fruit的List将持有任何类型的Fruit,诚然包括Apple,但是他不是一个Apple的List,它仍旧是Fruit的List。Apple的List在类型上不等价与Fruit的List,即使Apple是一种Fruit类型。
	真正的问题是容器的类型,而不是容器持有的类型。与数组不同,泛型没有内建的协变类型。这是因为数组在语言中是完全定义的,隐藏可以内建编译期和运行时的检查,但是在使用泛型时,编译器和运行时系统都不知道你想用类型做什么,以及应该采用什么规则。
	有时你想要在两个类型之间建立某种类型的向上转型关系,可以通过通配符。
	  public static void main(String[] args) {
        ArrayList<? extends Fruit> flist = new ArrayList<>();
        // Compile Error
//        flist.add(new Apple());
        // Compile Error
//        flist.add(new Fruit());
        // Compile Error
//        flist.add(new Orange());
        flist.add(null);
        Fruit fruit = flist.get(0);
    }
    flist类型现在是List<? extends Fruit>,读作“具有任何从Fruit继承的类型的列表”,但是,这实际上并不意味着这个List能持有任何类型的Fruit,通配符引用的是明确的类型,因此它意味着“某种flist引用没有指定的具体类型”。因此这个被赋值的List必须持有诸如Fruit或Apple这样的类型,但是为了向上转型成flist,这个类型是什么没人关系。
    因为我们不知道List持有什么类型,我们就不能安全向其中添加对象,因此编译期就会阻止我们添加对象。
    另一方面,如果我们调用返回Fruit的方法,则是安全的,因为这个List中的任何对象至少具有Fruit类型,因此编译器允许我们get(0).
2、编译器
	你可能以为自己被阻止去调用任何接受参数的方法,其实不然
	public class CompilerIntelligence {
	    public static void main(String[] args) {
	        List<? extends Fruit> flist = Arrays.asList(new Apple());
	        Apple apple = (Apple) flist.get(0);
	        System.out.println(flist.contains(new Apple()));
	        System.out.println( flist.indexOf(new Apple()));
	    }
	}
	contains和indexOf都可以接受Apple对象,并且可以正常执行。是否意味着编译器实际将检查代码,来查看是否有特定的方法修改了他的对象?
	通过ArrayList的文档,我们可以发现add接受一个泛型参数类型的参数,但是contains和indexOf将接受Object类型的参数,因此,当指定一个  ArrayList<? extends Fruit>时,add的参数就变成了? extends Fruit.因此,编译器并不能了解这里需要Fruit的哪个具体子类型,因此它不会接受任何类型的Fruit,如果将Apple向上转型为Fruit,编译器将直接拒绝对参数列表中涉及通配符的方法(add)的调用,也就是不能直接add(new Fruit())。
	在contains和indexOf,参数类型为Object,因此不涉及任何通配符,而编译器也将允许调用。意味着泛型类的设计者决定哪些调用时安全的,并用Object类作为参数类型。为了在类型中使用了通配符的情况下禁止这类调用,我们需要在参数列表中使用类型参数
	public class Holder<T> {
	    private T value;

	    public Holder() {
	    }

	    public Holder(T value) {
	        this.value = value;
	    }

	    public T getValue() {
	        return value;
	    }

	    public void setValue(T value) {
	        this.value = value;
	    }

	    @Override
	    public boolean equals(Object o) {
	        return value.equals(o);
	    }

	    @Override
	    public int hashCode() {
	        return Objects.hash(value);
	    }

	    public static void main(String[] args) {
	        Holder<Apple> appleHolder = new Holder<>(new Apple());
	        Apple value = appleHolder.getValue();
	        appleHolder.setValue(value);
	        // cannot upcast
	//        Holder<Fruit> fruit = appleHolder;
	        Holder<? extends Fruit> fruit = appleHolder;
	        Fruit f = fruit.getValue();
	        value = (Apple)fruit.getValue();
	        try{
	           Orange c =  (Orange)fruit.getValue();
	        } catch (Exception e){
	            System.out.println(e);
	        }
	        // cannot call set
	        fruit.setValue(new Apple());
	        // cannot call set
	        fruit.setValue(new Fruit());
	        System.out.println(fruit.equals(value));

	    }

	}
	如果创建了一个Holder<Apple>不能向上转型为Holder<Fruit>,但是可以向上转型为Holder<? extends Fruit>如果调用getValue,只会返回一个Fruit——在给定“任何扩展自Fruit的对象”这一边界后,它所能知道的一切了。如果你能够了解更多的信息,比如强制转换成Apple【某种具体的Fruit类型】,这不会导致任何警告,但是存在着ClassCastException【转成orange】。set方法不能作用于apple或Fruit,是因为setValue的参数也是“? extends Fruit”,这意味着它可以是任何事物,而编译器无法验证“任何事物”的类型安全性。
	但是equals由于它接受的是Object而非T类型,因此,编译器只关注传递进来和要返回的对象类型,它并不会分析代码,以查看是否执行了任何实际的写入和读取操作。
3、逆变
	超类型通配符,可以声明通配符是由某个特定类的任何基类来界定的,方法用<? super MyClass> 类型参数<? super T>尽管你不能对泛型参数给出一个超类型边界,即不能声明<T super MyClass>。super使得我们可以安全地传递一个类型对象到泛型类型中。
	static void writeTo(List<? super  Apple> apples){
        apples.add(new Apple());
        apples.add(new Jonathan());
        // Error
//        apples.add(new Fruit());
    }
    参数Apple是Apple的某种基类型的List,这样我们可以向其中安全添加Apple或Apple的子类型。既然Apple是下界,那么我们添加Fruit显然是不安全的,会导致这个List扩大接纳范围,从而可以向其中添加非Apple类型的对象,这是违反静态类型安全的。
    我们可以根据能够像一个泛型类型“写入”(传递给一个方法),以及从一个泛型类型中读取(从一个方法返回),来思考子类型和超类型边界。
    超类型边界放松了可以向方法传递的参数上所做的限制。
    public class GenericWriting {
	    static <T> void writeExact(List<T> list, T item) {
	        list.add(item);
	    }

	    static List<Apple> apples = new ArrayList<Apple>();
	    static List<Fruit> fruit = new ArrayList<Fruit>();
	    static void f1(){
	        writeExact(apples,new Apple());
	        // 未知版本会出现不自动向上转型而 出现Error
	        writeExact(fruit,new Fruit());
	    }
	    static <T> void writeWithWildcard(List<? super 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();
	    }
	}
	writeExact没有使用通配符可能导致不允许将Apple放到List<Fruit>中,即使知道这应该可以的。
	writeWithWildcard中,其参数为List<? super T>因此这个List将持有从T导出的某种具体类型。这样就可以安全地将一个T类型的对象或者从T导出的任何对象作为参数传递给List的方法。
	public class GenericReading {
	    static <T> T readExact(List<T> list){
	        return list.get(0);
	    }
	    static List<Apple> apples = Arrays.asList(new Apple());
	    static List<Fruit> fruits = Arrays.asList(new Fruit());
	    static void f1(){
	        Apple apple = readExact(apples);
	        Fruit fruit = readExact(fruits);
	        fruit = readExact(apples);
	    }
	    /**
	     *  如果是一个类,那么他的类型在这个类初始化完成后就被建立关系
	      */
	    static class Reader<T>{
	        T readExact(List<T> list){return list.get(0);}
	    }
	    static void f2(){
	        Reader<Fruit> fruitReader = new Reader<>();
	        Fruit fruit = fruitReader.readExact(fruits);
	        // compile error readExact(List<Fruit>) cannot be applied to List<Apple>
	//        Fruit a = fruitReader.readExact(apples);
	    }
	    static class CovariantReader<T>{
	        T readCovariant(List<? extends T> list){
	            return list.get(0);
	        }
	    }
	    static void f3(){
	        CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
	        Fruit fruit = fruitCovariantReader.readCovariant(fruits);
	        Fruit fruit2 = fruitCovariantReader.readCovariant(apples);
	    }

	    public static void main(String[] args) {
	        f1();f2();f3();
	    }
	}
	readExact使用了精确类型。因此如果使用这个没有任何通配符的精确类型,就可以向List写入和读取这个精确类型。对于返回值,静态的泛型方法可以有效地“适应”每个方法调用,并从List<Apple>返回Apple,List<Fruit>返回一个Fruit。因此,如果可以摆脱静态泛型方法,那么当只是读取时,就不需要协变类型了。
	但是,当我们使用泛型类,并创建这个类的实例时,要为这个类确定参数,从fruitReader中List<Fruit>可以读取一个Fruit,因为是它的确切类型,但是List<Apple>还应该产生Fruit对象,而fruitReader不允许这么做。
	为了解决上述问题,CovariantReader方法将接受List<? extends T>。因此从这个列表中读取一个T是安全的(你知道这个列表中的所有对象至少是一个T,并且可能是从T导出的对象)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值