Java与Scala的协变与逆变

来源:互联网 发布:后盾网 mysql视频教程 编辑:程序博客网 时间:2024/05/29 17:23

Java与Scala的协变与逆变

一、 概念介绍

  在Java与Scala中都支持协变、逆变与非转化。考虑一种场景,一个方法的参数类型为List[AnyVal],那我传入List[Int]是否符合要求呢?即List[Int]是否为List[AnyVal]的了类呢?如果是,这种转化则称为协变,如果List[Int]是List[AnyVal]的父类,则这种转化称为逆变。协变与逆变是里氏替换原则的一种表现,该规则适用于子类行为,而非超类行为,说白了,协变/逆变是为了在不同的场景更好的描述类的继承关系, 如下图:
协变逆变

  为了清楚的说明,看如下例子:

object CovAndContra {  def main(args : Array[String]): Unit ={    var leo1 : CovAndContra[A] = new CovAndContra[A](new A)    var leo2 : CovAndContra[B] = new CovAndContra[B](new B)    var leo3 : CovAndContra[C] = new CovAndContra[C](new C)    leo1 = leo2    leo3 = leo2   //编译错误  }}class A {}class B extends A{}class C extends B{}class CovAndContra[+A]( c : A){}

  类A<-B<-C依次为继承关系,如果定义CovAndContra 是协变的,则leo*的继承关系为leo1<-leo2<-leo3。当CovAndContra是逆变的,即CovAndContra[-A]则leo*的继承关系为leo3<-leo2<-leo1。注:在Java中参数化类型在定义时并不支持继承转化行为,说白了,就是Java不能在定义一个类的时候使用协变/逆变的特性。类型的转化标记说明:

Scala Java 描述 +T ? extends T 协变 -T ? super T 逆变 T T 非转化继承

  协变相对好理解,下面以一个Java的例子来说明在什么情况下使用逆变。

public class JavaCovAndContra<T> {    public static void main(String[] args){        List<JA> leo1 = new ArrayList<>();        List<JB> leo2 = new ArrayList<>();        List<JC> leo3 = new ArrayList<>();        JavaCovAndContra<JB> leo = new JavaCovAndContra<>();        leo.pull(leo1);        leo.pull(leo2);        leo.push(leo2);        leo.push(leo3);        leo.pull(leo3);    //报错        leo.push(leo1);    //报错    }    private List<T> arr = new ArrayList<>();    public void push(List<? extends T> list){        for(T item : list){            arr.add(item);        }    }    public void pull(List<? super T> list){        for(T item : arr){            list.add(item);        }    }}class JA {}class JB extends JA {}class JC extends JB {}

二、 协变/逆变与函数的参数

  在《Programming in Scala》中有这样两句话,“1、变异标记只有在类型声明中的类型参数里才有意义,对参数化的方法没有意义,因为该标记影响的是子类继承行为,而方法没有了类。2、函数的参数必须是逆变的,而返回值必须是协变的。”小编感觉,把这两句话理解透彻就可以很好的掌握协变/逆变的特性了。先说第一句,如第一个小节里所说,协变/逆变只使用在类型参数中,因此在方法在使用标记是错误的。如下的方法声明是不允许的。

Scala : def test(a : +A) : Unit = ???Java : public void test(? extends T list)

  而一个疑问是Java的代码中public void push(List

class A {}class B extends A{}class C extends B{}//类一class CovAndContra[+T]( c : T){  def getValue : T = ???  def setValue(value : T) : Unit = ??? //编译报错}//类二class CotraAndCov[-T]( c : T){  def getValue : T = ???  //编译报错  def setValue(value : T) : Unit = ???}

  如上,两个例子,类一与类二分别为协变与逆变的,两个类分别实现了两个方法,并分别有一个方法在编译时是报错的。我们假设两个类都可以通过编译,下面来看看,这会导致怎样的问题。声明一个对象为x= CovAndContra[B] 这时T=B,再声明一个对象为y= CovAndContra[C] 这时T=B,由于CovAndContra是协变的,因此可以做x=y这样的赋值,这时x的类型是不变的为CovAndContra[B],而其实际指向的类型为CovAndContra[C],这时我们调用getValue类型,返回B类型,实际返回的是C类型,由于B<-C,因此是符合逻辑的,而当我们调用setValue类型时,我们想处理B类型,而实现调用的方法只能处理C类型,所以这时就会有问题。再详细一下,B类型为一个方法为funB,而C类型除了实现funB外,又添加了funC方法,对于y= CovAndContra[C]这个对象,我们调用y.setValue时,传入的是类型C,存在funC方法,而当我们把y赋值给x时,由于我们调用x.setValue时传入B类型就可以,而setValue的实现处理方法是CovAndContra[C]这个类实现的,如果setValue方法中调用了funC方法,这时就会出问题了。

三、 类型的上界,下界

  在Java中,类型的上下界通过extends与super来实现,可以通过如下方法定义类型。

class JA<T extends String> {}

  而scala更灵活,可以对方法指定参数的输入类型范围(java是否可以,这个不太了解,应该是不可以的),方法定义如下:

def setValue[X <: String](value : X) : Unit = ???

  对于协变/逆变的+-与<: >:符号的区别,个人认为协逆变的-+是继承的标识,以在编译时告诉编译器该类型的协逆变的,当然也可以确定类型的边界,而>: <:只是用于确定范围的。即如果我们这样定义类型:

class CovAndContra[T <: A]( c : T)var leo1 : CovAndContra[A] = new CovAndContra[A](new A)var leo2 : CovAndContra[B] = new CovAndContra[B](new B)leo1 = leo2 //报错

当做leo2到leo1的赋值时,会有报错.

0 0