Java中的协变与逆变

  Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承)。

  在继承派生的过程中,是符合Liskov替换原则(LSP)的。LSP总结起来,就一句话:

    所有引用基类(父类)的地方必须能够透明地使用其子类的对象。

  LSP包含四层含义:

    ① 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法;

    ② 子类中可以增加自己的方法;

    ③ 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更加宽松;

    ④ 当子类覆盖或实现父类的方法时,方法的返回值要比父类方法的更加严格。

 

  针对LSP四层含义的③④条,就引出了协变(Covariance)和逆变(Contravariance)的概念:

    协变,简言之,就是父类型到子类型,变得越来越具体,在Java中体现在返回值类型不变或更加具体(异常类型也是如此)等。

    逆变,简言之,就是父类型到子类型,变得越来越具体,但是方法的形参却变得更加抽象或不变(注意:这里在Java中本质为方法重载,而不是覆盖,当添加@Override标签将会报错!)

 

  于是,针对上述的逆变,概念可以理解,但是Java中所谓的“方法形参变得更加宽泛”,实质是方法重载,似乎也就不能严格地称为继承关系下的“逆变”,思考之,似乎Java的继承派生过程中,几乎所有的操作,都是协变的,那么是不是就说明Java中并不存在逆变呢???

  错!Java中是存在逆变的!

 

  这里就引出了Java中的泛型,这里举个例子:

  在Java中,Number类是Integer类的父类(super),如果某个方法的签名是void method(List<Number> listNumber),那么按照协变的思想,是不是意味着为这个方法传入List<Integer>类型参数也是可以的呢?

  当然不... 很多博客在此都说“Java对于这样的泛型是不支持协变的”,但我认为,事实是List<Integer>的实参,依旧是一个List类型的持有对象,因此对于List<Number>这个持有对象来说,二者持有的对象存在继承派生的关系,但二者本身并不存在继承派生的关系,因而也就无从谈及协变(实质,这里二者的关系是“不变”,“不变”是针对于协变与逆变概念而言的)。

  举个例子来说明这个简单的问题,一个父亲和他的儿子都分别有一辆车,他们的车款型相同(当然,也可能不同,但总归是车,即持有对象,这里为了针对上述二者持有对象均为List故意言之),尽管车上的父亲和儿子存在着“继承派生”的关系,但是这两辆车并不存在继承关系,所以二者之间并没有“协变”的概念。

  那么,总不能对于这样的method,要为每种持有对象持有的对象类型分别重载实现method吧...于是,Java就提供了泛型的通配符(注意,这里才谈到泛型),为了解决上述的method问题,可以这样声明method: void method(List<? extends Number> listNumber)。这样,这里的形参就必须是一个持有对象,它持有的对象类型,必须是Number类或者是继承自Number类的更具体的子类(如Integer类,Double类),此时,可以说这个方法依旧实现了“协变”,那么Java中的逆变是体现在哪儿的呢?

  这里就引出了通配符后另一个关键字,super。

  这样声明的方法:void method(List<? super Integer> listInteger),说明该持有对象持有的对象类型,必须是Integer或Integer的父类(超类super),于是,此时向方法中传递持有Number类的持有对象也是可以的,甚至,可以传递一个持有Object类型的持有对象。此处便是使得参数类型变得更加宽泛,因此此处体现的是“逆变”。

  这也很好记:

    ? extends 对应 协变

    ? super    对应 逆变

    (? 即为Java泛型的通配符)

  综上,Java是符合LSP的一门语言,对“协变”“逆变”的支持也是有具体实现以及道理的。理解好这些概念,可以让编程中遇到的知识概念更加系统化,理解记忆也更高效。

  至于前几天,有同学在群里讨论,Java中如果子类覆盖了父类的方法,是否就不符合LSP了,如果说Java是严格按照LSP来设计的,那么这种情况是否就不能称为覆盖,而是重载...

  当时被这个问题雷到了... 我理解的LSP应该是一种思想,是设计过程以及实现过程中开发者应该牢记并遵守的。如果按照LSP的总的规则,那么每个父类对象出现的地方,都可以用其具体子类对象来替换而不会发生错误。这个错误当然是保证语法编译不会发生错误,而不是针对覆盖方法导致的功能不同。所以...这个问题,实质上应该归结于理解发生了偏颇...

  本博客参考博客:

  Java协变和逆变:https://blog.csdn.net/qiuchengjia/article/details/52910901

  Java的逆变与协变:https://www.cnblogs.com/en-heng/p/5041124.html

 

  from Steven Shen

    编辑于2018.6.22

    修改于2019.9.4

  

转载于:https://www.cnblogs.com/stevenshen123/p/9215750.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java泛型都是针对类型转换的规定。 (covariant):指的是继承链子类(派生类)类型能够作为父类(基类)类型的一种属性,也就是子类可以作为父类使用的能力。在泛型的概念可以用来表示如果类型A是类型B的一个子类型,那么泛型类G<A>就可以视作泛型类G<B>的一个子类型。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口List public interface List<E> { void add(E e); E get(int index); } // 定义一个方法acceptList,其形参类型为List<? extends Animal> public static void acceptList(List<? extends Animal> list) { for (Animal animal : list) { // ... } } // List类型为List<Dog> List<Dog> list = new ArrayList<Dog>(); list.add(new Dog()); acceptList(list); // 在这里,我们可以传入一个List<Dog>参数,因为Dog类是Animal类的子类 ``` (contravariant):指的是继承链父类(基类)类型能够作为子类(派生类)类型的一种属性,也就是父类可以作为子类使用的能力。在泛型的概念可以用来表示如果类型A是类型B的一个超类型,那么泛型类G<B>就可以视作泛型类G<A>的一个子类型。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口Comparator public interface Comparator<T> { int compare(T o1, T o2); } // 定义一个方法sortList,其形参类型为List<? super Dog> public static void sortList(List<? super Dog> list) { // ... } // List类型为List<Animal> List<Animal> list = new ArrayList<Animal>(); list.add(new Animal()); sortList(list); // 在这里,我们可以传入一个List<Animal>参数,因为Animal类是Dog类的超类型 ``` extends和super关键字常常用于定义泛型类型参数的上边界(upper bound)和下边界(lower bound)。extends表示类型参数的上限,超过这个范围就会导致编译错误;super表示类型参数的下限,超过这个范围也会导致编译错误。 例子: ```java // 泛型类Pair,其类型参数T有上限(用extends)为Comparable<? super T>,表示类型T要么是Comparable<? super T>本身,要么是Comparable<? super T>的子类型 public class Pair<T extends Comparable<? super T>> { private T first; private T second; public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public T max() { return first.compareTo(second) >= 0 ? first : second; } } // Pair类型为Pair<String> Pair<String> pair = new Pair<String>("hello", "world"); String max = pair.max(); // 在这里,我们可以调用max方法,因为String类实现了Comparable<String>接口 ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值