Java与Scala的协变与逆变
2017-04-07 15:44
274 查看
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 | 非转化继承 |
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的赋值时,会有报错.
相关文章推荐
- Java、Scala、C#泛型中的协变和逆变
- Java语言中的协变和逆变
- Java中的逆变与协变
- scala中的上界、下界、协变和逆变
- Java的协变(extends)和逆变(super),说白了都是子类的实例赋值给父类的变量
- Java中的逆变与协变
- scala类型系统之协变与逆变
- Java泛型里的协变和逆变
- Programming In Scala笔记-第十九章、类型参数,协变逆变,上界下界
- 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解
- Scala之协变和逆变
- [置顶] Java中的逆变与协变
- scala的协变逆变,上界下界
- JAVA中有关逆变和协变类型的详解
- scala-协变、逆变、上界、下界
- Java进阶知识点2:看不懂的代码 - 协变与逆变
- Scala中的协变,逆变,非变,上界,下界
- Scala类型参数中协变(+)、逆变(-)、类型上界(<:)和类型下界(>:)的使用
- scala类型系统:15) 协变与逆变