Java中的逆变与协变

栏目: Java · 发布时间: 7年前

内容简介:如果B是A的子类,并且F(B)也是F(A)的子类,那么F即为协变如果B是A的子类,并且F(B)成了F(A)的父类,那么F即为逆变Java中的泛型有逆变和协变两种操作,定义如下:

什么是逆变与协变

协变(Covariance)

如果B是A的子类,并且F(B)也是F(A)的子类,那么F即为协变

逆变(Contravariance)

如果B是A的子类,并且F(B)成了F(A)的父类,那么F即为逆变

Java中的逆变与协变

Java中的泛型有逆变和协变两种操作,定义如下:

协变

<? extends A>

B是A的子类,那么List< B >是List<? extends A>的子类

逆变

<? supper A>

B是A的子类,那么List< B >是List<? super A>的父类

Java中逆变与协变的约束

Java的协变逆变及其约束,都是出于对多态的应用。为了后续说明方便,这里先定义一系列的父子类

class Fruit {
    public String returnMeat() {
        return "generic fruit meat";
    }
}

class Apple extends Fruit {
    @Override
    public String returnMeat() {
        return "apple meat";
    }
}

class GreenApple extends Apple {
    @Override
    public String returnMeat() {
        return "green apple meat";
    }
}

多态

Java是支持多态的。如果一个方法的参数接收的是A类型,那么将其子类型作为参数,调用该方法,依然可行。

例如eatFruitMeat方法就能体现多态特性

@Test
    public void test1() {
        eatFruitMeat(new Fruit());//输出eat generic fruit meat
        eatFruitMeat(new Apple());//输出eat apple meat
        eatFruitMeat(new GreenApple());//输出eat green apple meat
    }

    public void eatFruitMeat(Fruit fruit) {
        System.out.println("eat "+fruit.returnMeat());
    }

协变约束

协变方法支持对传入参数的读操作,但不支持修改操作。如下:

@Test
    public void test1() {
        List<GreenApple> greenApples = Lists.newArrayList(new GreenApple());
        List<Fruit> fruits = Lists.newArrayList(new Fruit());
        List<Apple> apples = Lists.newArrayList(new Apple());
        eatFruitMeats(greenApples);
        eatFruitMeats(fruits);//编译错误1
        eatFruitMeats(apples);
    }

    public void eatFruitMeats(List<? extends Apple> fruits) {
        fruits.forEach(fruit->System.out.println("eat "+fruit.returnMeat()));
        fruits.add(new Apple());//编译错误2
        fruits.add(new Fruit());//编译错误3
        fruits.add(new Object());//编译错误4
    }
  • 编译错误1: eatFruitMeats方法接受的List<? extends Apple>的子类,显然List<Fruit>不是其子类
  • 编译错误2,3,4: eatFruitMeats方法在被调用前,并不知道最终调用方,传递进来的具体是哪一个子类?有可能是List< Apple >,也有可能是List< GreenApple >,所以贸然向其中添加任何对象,都是可能出错,比如你不能把一个Apple对象放进List< GreenApple >。为了防止这些可能的错误,编译器提前进行了约束限制。

逆变约束

逆变主要在写的场景,即只能向逆变容器中添加,下界类型本身或其子类

@Test
public void test1() {
    List<Fruit> fruits = Lists.newArrayList();
    List<Apple> apples = Lists.newArrayList();
    List<GreenApple> greenAppleLists = Lists.newArrayList();

    collectFruits(fruits);
    collectFruits(apples);
    collectFruits(greenAppleLists);//编译错误1
}

public void collectFruits(List<? super Apple> fruits) {
    fruits.add(new Fruit());//编译错误2
    fruits.add(new Apple());
    fruits.add(new GreenApple());
}
  • 编译错误1: 由于是逆变,所以List<GreenApple>是List<? super Apple>的父类。所以不能将greenAppleLists作为参数调用collectFruits方法,因为不满足 Java 方法参数的多态性要求,即只能传本类或子类的要求
  • 编译错误2: 如果调用方传递的是List< Apple >,那往其中添加父类Fruit对象,在运行时肯定会报错,为了避免这种情况,编译器提前报错。

总结

Java泛型支持协变和逆变,具体在使用时,会有一些约束。这些约束,需要从Java语言的特性,比如多态性,以及运行时安全性去理解。

简单总结协变、逆变参数的方法调用特点如下:

协变参数

  • 只接受自己的子类。协变的父子关系,同类原本的父子关系一致。如GreenApple是Apple的子类,List< GreenApple >是List<? extends Apple>的子类
  • 对写有约束,只能用于读

逆变参数

  • 只接受自己的子类。逆变的父子关系,同类原本父子关系相反。如GreenApple是Apple的子类,List< GreenApple >是List<? super Apple>的父类
  • 只能写入下界的子类,本例中,只能向List中写入Apple及Apple的子类

参考链接

https://medium.com/@sinisalouc/variance-in-java-and-scala-63af925d21dc


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Head First Web Design

Head First Web Design

Ethan Watrall、Jeff Siarto / O’Reilly Media, Inc. / 2009-01-02 / USD 49.99

Want to know how to make your pages look beautiful, communicate your message effectively, guide visitors through your website with ease, and get everything approved by the accessibility and usability ......一起来看看 《Head First Web Design》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

在线进制转换器
在线进制转换器

各进制数互转换器

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码