侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 285 篇文章
  • 累计创建 125 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

Java进阶知识点6:泛型之协变和逆变

孔子说JAVA
2022-05-26 / 0 评论 / 0 点赞 / 88 阅读 / 7,560 字 / 正在检测是否收录...

1、协变,逆变,不可变

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≦表示继承关系(比如,A≦B表示A是由B派生出来的子类):

  • 当A ≦ B时,如果有f(A) ≦ f(B),那么f是协变(covariant)的;
  • 当A ≦ B时,如果有f(B) ≦ f(A),那么f是逆变(contravariant)的;
  • 如果上面两种关系都不成立,即(A)与f(B)相互之间没有继承关系,则叫做不变(invariant)的。

假设有如下类:

class Food{} // 默认继承Object
class Fruit extends Food{}
class Meat extends Food {}

class Apple extends Fruit{}
class Beef extends Meat{}

image-1652752978412

例如数组是斜变的:

Food food = new Fruit();  
food = new Meat(); // 即 把子类赋值给父类引用

Fruit [] arrFruit = new Fruit[3];
Food [] arrFood = arrFruit; // 数组协变的

java 的泛型是不可变的:

List<Beef> beefList = new ArrayList<>();
List<Food> foodList = beefList; //错误:不可协变  
beefList = foodList; // 错误 : 不可逆变

eat(beefList);// 错误::不可协变  

public void addFood(List<Food> list){
    list.add(new Apple());
}

为什么不能把beefList 作为参数传递给 addFood(List<Food> list) 方法呢?这儿我们用反正法类证明:

addFood 方法接收 List<Food> list 的类型参数,那么在addFodd 方法中能给 传递进来的 List add 任何 Food 的子类型。那么,既然eat(beefList) 这行代码没有问题,也就意味着能通过addFood 方法给 beefList 里面增加任何 Food 的子类型(比如:list.add(new Apple()) ).

通过第一步,我们给 装肉beefList 里面加了一个苹果(Apple), 那么从beefList get 数据赋值给Beef 的时候(Beef beef = beefList.get(0))就会发生类型转换异常 ,因为取出来的是一个Apple, 这样明显有问题了。

因此:得出java中泛型是不变得,既不能协变,也不能逆变。

2、泛型中的通配符–实现泛型的协变与逆变

Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场:

<? extends T> 实现了泛型的协变,比如:

List<? extends Food> foodList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
foodList = appleList; // ok 协变

foodList.add(new Beef()); // 错误 不能执行添加null 以外的操作
foodList.add(new Food());// 错误,同上,
foodlist.add(new Apple()); // 错误,同上

Food food = foodList.get(index); //ok, 把子类引用赋值给父类显然是可以的

extends指出了泛型的上界为Food,即表示了集合中存放的对象是 Food 或者 Food 的子类,<? extends T> 称为子类通配符,意味着某个继承自Food 的具体类型。使用通配符可以将 ArrayList<Apple> 向上转型了,也就实现了协变。appleList 集合就是一个存放Food 的子类 Apple的集合, 因此可以把 appleList 赋值给 foodList,但是不能对foodList 添加除null 以外的任何对象。为什么不能添加呢?其实很简单,如果可以允许的话,那么foodList 就可以添加Food 或者Food 的子类型,那么上面代码中在 foodList = appleList; 赋值后,执行 foodList.add(new Beef()); 操作就会导致给 appleList 里面添加了一个Beef 对象,显然这样是不对的。

<? super> 实现了泛型的逆变,比如:

List<? super Fruit> fruitList = new ArrayList<>();
List<Food> foodList = new ArrayList<>();
fruitList = foodList; // ok 逆变

foodList.add(new Meat()); 

fruitList.add(new Apple()); // ok,只能添加 Fruit 或者 其子类
fruitList.add(new Food());// error, 只能添加 Fruit 或者 其子类

Fruit fruit = fruitList.get(0); // error,get出来的元素是Object类型
Object obj = fruitList.get(0);// ok 

<? super Fruit> 指明了泛型的下界为Fruit,即表示了集合中存放的对象只能是 Fruit 或者其父类,<? super T> 称为超类通配符,代表一个具体类型,而这个类型是Fruit的超类。因此fruitList 表示了一个存放Fruit 或者其父类的集合,那么就可以把一个Fruit 父类Food 的集合foodList 赋值给 fruitList(即:fruitlist = foodList )。

这儿可能有的人就有点疑问了?<? super Fruit> 指明了集合存放的对象是Fruit 或者其父类,那为什么往集合中添加Fruit的父类元素不行呢(fruitList.add(new Food()))?同样我们用反正法来说明,假如可以向 <? super E> 集合中添加 E 的父类,那么就可以向fruitList 添加Fruit 的父类(Food or Object),在本例中,我们把foodList 赋值给了fruitList(fruitList = foodList),既然可以添加父元素,那么我们执行fruitList.add(new Food) 就相当于给 foodList集合里面添加了一个Food元素,这看起来没有什么问题,因为foodList 本来就是用来装Food 元素的,但是如果我们添加的Fruit 父类是Object 对象(或者一个Fruit 实现的和Food 无关的接口,又或者Food继承的一个其他类B,那么B也是 Fruit 的父类),那么就意味着给foodList 里面添加了和Food 无关的类,这样显然是不行的。那么为什么往fruitList 里面添加Fruit或者其子类 元素是可以的,因为 fruitList 表示的就是一个存放Fruit 或者其父类的集合,所以这个集合肯定就能装Fruit 或者其子类,比如上面把foodList 赋值给fruitList 后,往fruitList 里面添加 Apple 就相当于往foodList 里面添加Apple,显然是可以的,对于get 操作时为什么读取出来的是Object ,是因为 fruitList 集合表示的存放Fruit 或者其父类的集合,而Fruit 的父类可能有很多,在本例中由于我们知道存的是Food(fruitList = foodList), 但是在实际中,谁知道在运行时到底存的是哪一个呢?因此为了安全全部定义为Object 类型。因此得出结论:

  • <? super E> 支持逆变,能往集合中添加E或者E子类的对象。不能添加E的任何父类对象,读取时的对象为Object 类型。
  • 上限通配符(子类通配符)? extends T 的用法,? extends T 表示所存储类型都是T及其子类,但是获取元素所使用的引用类型只能是T或者其父类。使用上限通配符实现向上转型,但是会失去存储对象的能力。上限通配符为集合的协变表示。
  • 下限通配符(超类通配符) ? super T 表示所存储类型为T及其父类,但是添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object,因为Object为所有类的基类。下限通配符为集合的逆变表示。

3、PECS

什么时候使用extends,什么时候使用super。《Effective Java》给出精炼的描述:producer-extends, consumer-super(PECS)。

说直白点就是,从数据流来看,extends是限制数据来源的(生产者),而super是限制数据流入的(消费者)。例如上面例子中,使用 <? super Fruit> 就是限制add方法传入的类型必须是Fruit及其子类型。

java.util.Collections的copy方法

// Collections.java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
        throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

copy方法限制了拷贝源src必须是T或者是它的子类,而拷贝目的地dest必须是T或者是它的父类,这样就保证了类型的合法性。

PECS总结:

  1. 要从泛型类取数据时,用extends;
  2. 要往泛型类写数据时,用super;
  3. 既要取又要写,就不用通配符(即extends与super都不用)。

4、自限定的类型

Java泛型中,有一个好像是经常性出现的惯用法,它相当令人费解。

class SelfBounded<T extends SelfBounded<T>> { // ...

SelfBounded类接受泛型参数T,而T由一个边界类限定,这个边界就是拥有T作为其参数的SelfBounded,看起来是一种无限循环。

先给出结论:这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域。为了理解这个含义,我们从一个简单的版本入手。

// BasicHolder.java
public class BasicHolder<T> {
    T element;
    void set(T arg) { element = arg; }
    T get() { return element; }
    void f() {
        System.out.println(element.getClass().getSimpleName());
    }
}

// CRGWithBasicHolder.java
class Subtype extends BasicHolder<Subtype> {}

public class CRGWithBasicHolder {
    public static void main(String[] args) {
        Subtype st1 = new Subtype(), st2 = new Subtype();
        st1.set(st2);
        Subtype st3 = st1.get();
        st1.f();
    }
}  
/* 程序输出
Subtype
*/

新类Subtype接受的参数和返回的值具有Subtype类型而不仅仅是基类BasicHolder类型。所以自限定类型的本质就是:基类用子类代替其参数。这意味着泛型基类变成了一种其所有子类的公共功能模版,但是在所产生的类中将使用确切类型而不是基类型。因此,Subtype中,传递给set()的参数和从get() 返回的类型都确切是Subtype。

5、自限定与协变

自限定类型的价值在于它们可以产生协变参数类型——方法参数类型会随子类而变化。其实自限定还可以产生协变返回类型,但是这并不重要,因为JDK1.5引入了协变返回类型。

下面这段代码子类接口把基类接口的方法重写了,返回更确切的类型。

// CovariantReturnTypes.java
class Base {}
class Derived extends Base {}

interface OrdinaryGetter { 
    Base get();
}

interface DerivedGetter extends OrdinaryGetter {
    Derived get();
}

public class CovariantReturnTypes {
    void test(DerivedGetter d) {
        Derived d2 = d.get();
    }
}

继承自定义类型基类的子类将产生确切的子类型作为其返回值,就像上面的get()一样。

// GenericsAndReturnTypes.java
interface GenericsGetter<T extends GenericsGetter<T>> {
    T get();
}

interface Getter extends GenericsGetter<Getter> {}

public class GenericsAndReturnTypes {
    void test(Getter g) {
        Getter result = g.get();
        GenericsGetter genericsGetter = g.get();
    }
}

协变参数类型

在非泛型代码中,参数类型不能随子类型发生变化。方法只能重载不能重写。见下面代码示例。

// OrdinaryArguments.java
class OrdinarySetter {
    void set(Base base) {
        System.out.println("OrdinarySetter.set(Base)");
    }
}

class DerivedSetter extends OrdinarySetter {
    void set(Derived derived) {
        System.out.println("DerivedSetter.set(Derived)");
    }
}

public class OrdinaryArguments {
    public static void main(String[] args) {
        Base base = new Base();
        Derived derived = new Derived();
        DerivedSetter ds = new DerivedSetter();
        ds.set(derived);
        ds.set(base);
    }
}
/* 程序输出
DerivedSetter.set(Derived)
OrdinarySetter.set(Base)
*/

但是,在使用自限定类型时,在子类中只有一个方法,并且这个方法接受子类型而不是基类型为参数。

interface SelfBoundSetter<T extends SelfBoundSetter<T>> {
    void set(T args);
}

interface Setter extends SelfBoundSetter<Setter> {}

public class SelfBoundAndCovariantArguments {
    void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
        s1.set(s2);
        s1.set(sbs);  // 编译错误
    }
}

捕获转换

<?> 被称为无界通配符,无界通配符有什么作用这里不再详细说明了,理解了前面东西的同学应该能推断出来。无界通配符还有一个特殊的作用,如果向一个使用 <?> 的方法传递原生类型,那么对编译期来说,可能会推断出实际的参数类型,使得这个方法可以回转并调用另一个使用这个确切类型的方法。这种技术被称为捕获转换。下面代码演示了这种技术。

public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }
    static void f2(Holder<?> holder) {
        f1(holder);
    }
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        Holder raw = new Holder<Integer>(1);
        f2(raw);
        Holder rawBasic = new Holder();
        rawBasic.set(new Object());
        f2(rawBasic);
        Holder<?> wildcarded = new Holder<Double>(1.0);
        f2(wildcarded);
    }
}
/* 程序输出
Integer
Object
Double
*/

捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。注意,不能从f2()中返回T,因为T对于f2()来说是未知的。捕获转换十分有趣,但是非常受限。

6、总结

  1. 数组是协变的。
  2. extends关键字加持的泛型是协变的,所存储类型都是T及其子类,获取元素所使用的引用类型只能是T或者其父类,只能添加null元素。
  3. super关键字加持的泛型是逆变的,所存储类型为T及其父类,添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object。
  4. 注意数组和泛型容器中添加和获取元素的类型限制。
0

评论区