泛型逆变

逆变

除了使用extends通配符外,还可以使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>,甚至或者使用类型参数:<? super T>(尽管不能对泛型参数给出一个超类型边界);即不能声明<T super MyClass>。这使得可以安全地传递一个类型对象到泛型参数中。因此,有了超类型通配符,就看向Collection写入了。

public class SuperTypeWildcards {
    static void writeTo(List<? super Apple> apples) {
        apples.add(new Apple());
        apples.add(new Jonathan());
        // apples.add(new Fruit()); Error
    }
}

参数applesApple的某种基类型的List,所以向里面添加Apple或者Apple的子类型是安全的。但是既然Apple是下界,那么向这样的List添加Fruits是不安全的。因为这会使List敞开口子,从而可以向里面添加非Apple类型的对象,而这样是违反静态类型安全的。
因此代码中可能会根据如何能够向一个泛型类型“写入”(传递给一个方法),以及如何能够从一个泛型类型中“读取”(从一个方法中返回),来着手思考子类型和超类型边界。
超类型边界放松了在可以向方法传递的参数上的限制。

public class GenericWriting {
    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }

    static List<Apple> apples = new ArrayList<>();
    static List<Fruit> fruits = new ArrayList<>();

    static void f1() {
        writeExact(apples, new Apple());
        writeExact(fruits, new Apple());
    }

    static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item);
    }

    static void f2(){
        writeWithWildcard(apples,new Apple());
        writeWithWildcard(fruits,new Apple());
    }

    public static void main(String[] args) {
        f1();
        f2();
    }
}

上面的代码中,f1()f2()都能很好的工作(在Java7版本之后)。我们可以很安全的将Fruit及其子类型作为'Fruit或者<? super Fruit>`的参数。
下面是一段类型分析。

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 a = readExact(apples);
        Fruit f = readExact(fruits);
        f = readExact(apples);
    }

    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }

    static void f2() {
        Reader<Fruit> fruitReader = new Reader<>();
        Fruit f = fruitReader.readExact(fruits);
        // Fruit a = fruitReader.readExact(apples); Error
    }

    static class CovariantReader<T> {
        T readCovariant(List<? extends T> list) {
            return list.get(0);
        }
    }

    static void f3() {
        CovariantReader<Fruit> fruitReader = new CovariantReader<>();
        Fruit f = fruitReader.readCovariant(fruits);
        Fruit a = fruitReader.readCovariant(apples);
    }

    public static void main(String[] args) {
        f1();
        f2();
        f3();
    }
}

上面的代码中,f1()用了精确的类型,因此如果使用了这个没有任何通配符的精确类型,就可以向List中写入和读取这个精确类型。另外,对于返回值,静态的泛型方法readExact()可以有效地“适应”每个方法调用,并能够从List<Apple>中返回一个Apple,从List<Fruit>中返回一个Fruit,就像在f1()中看到的那样。因此,只是读取的时候,静态的泛型方法不需要协变类型。
但是,如果有个泛型类,那么当创建这个类的实例的时候,要为这个类确定类型参数。就像在f2()中看到的那样,fruitReader可以从List<Fruit>中读取一个Fruit,因为这就是它的确切类型,但是List<Apple>还应该产生Fruit对象,但是fruitReader不允许传入List<Apple>参数,因为参数是List<Fruit>类型,编译器不允许发生这种转换。
而为了修正这个问题,CovariantReader.readCovariant()方法将接受List<? extends T>,因此,从这个列表中读取一个T是安全的(因为这个列表的所有对象至少是一个T,并且可能是T导出来的对象),所以在f3()中,可以从List<Apple>中读取Fruit了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 前言 泛型(Generics)的型变是Java中比较难以理解和使用的部分,“神秘”的通配符,让我看了几遍《Java...
    珞泽珈群阅读 7,913评论 12 51
  • 2.简单泛型 -********Java泛型的核心概念:告诉编译器想使用什么类型, 然后编译器帮你处理一切细节 2...
    CodingHou阅读 398评论 0 0
  • 第8章 泛型 通常情况的类和函数,我们只需要使用具体的类型即可:要么是基本类型,要么是自定义的类。但是在集合类的场...
    光剑书架上的书阅读 2,158评论 6 10
  • 引言 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用。本文我们将从零开始来看一下Jav...
    横冲直撞666阅读 429评论 0 0
  • 看到烟农在采摘烟叶 想到烟叶炼狱般的一生 刚成熟就被剥离母体 然后在高温烘烤下 抽干最后一丝水分 身体在铡刀下四分...
    致远_8493阅读 168评论 0 1