现在的位置: 首页 > 综合 > 正文

java 协变性 逆变性 学习笔记

2017年10月22日 ⁄ 综合 ⁄ 共 3003字 ⁄ 字号 评论关闭

在面向对象的计算机程序语言中,经常涉及到类型之间的转换,例如从具体类小猫到动物之间的类型转换(上行转换),或者从形状向三角形之间的转换(下行转换)。
我们之前往往都比较关注类型本身,却常常忽略类型转换的性质。最近在拜读《thinking in java》看到一些关于类型转换性质的比较抽象理论的内容,仔细研究一下,分享给大家。
我们将围绕如下三个核心名词:协变性(covariance)、逆变性(contravariance)和无关性(invariant)。他们都是用来描述类型转换的性质的术语,形式化的描述如下:

如果A和B是类型,f表示类型转换,≤表示子类型关系,(例如A≤B,表示A是B的子类)那么:
如果A≤B 则f(A) ≤ f(B) 那么 f是协变的
如果A≤B 则f(B) ≤ f(A) 那么 f 是逆变的

如果上面两种都不成立,那么f是无关的

例如java中,f(A) = List<A> ,这里List声明如下:
class List<T>{…}
我们可以理解泛型类List输入一个类型参数T,然后将T转换成类型List<T>,这个过程本身是一个类型转换f。
我们举例A = Object,B=String,经过类型变换f以后f(A)=List<Object>,f(B) = List<String>。
String ≤ Object 但是 f(A) ≤ f(b)却不成立。
所以上面的类型转换是无关的。

在Java语言中,有一些语法可以用协变性和逆变性来理解。
数组
例如 A = String, B = Object 我们这里有A≤B
f(A) = String[], f(B) = Object[]. 因为Object[] objects = new String[n]. 所以我们可以认为数组具有协变性。

X = Y

1. 协变返回值

在面向对象语言中,一个协变返回值方法是一个在子类覆盖该方法的时候,方法的返回值可以被一个“更窄”的类型所替代。
(C#并不支持这个技术,C++和Java JDK5.0后开始支持)

例如:

public static class Super {  
      Object getSomething() {  
      return null;  
   }  
} 
public static class SubClass extends Super {  
@Override  
     String getSomething() {  
           return null;  
     }  
}

这段话主要的思想就是协同返回值使用在子类需要一个不同于父类覆盖方法的返回值,但是返回值却有继承关系的情况下。
一个可能更好的例子如下所示:

class Collection {  
     Iterator iterator() { ... }  
}  
  
class List extends Collection {  
    @Override   
    ListIterator iterator() { ... }  
}

Iterator函数获得到当前集合的迭代器,在子类中,迭代器有着更确切的表示,所以使用了Iterator的子类ListIterator作为新的返回值。


2. 逆变参数

先举个例子,可以一下子明白逆变参数的例子,其实逆变参数使用环境依然是在继承体系下的函数覆盖。

class Super {  
          void doSomething(String parameter) {  
          }  
}  
  
class Sub extends Super {  
        void doSomething(Object parameter) {  
         }  
} 

为了更好的理解这个问题,首先引入Liskov Substitution Principle的概念。这个原则的内容就是说,子类的对象可以在任何需要父类对象的地方使用。
这个原则和参数以及返回值有什么关系呢?
假如我们定义类如下:

class Super {  
       ReturnType doSomething(ParameterType a) {  
            return null;  
       }  
}  
  
class Sub extends Super {  
       ReturnType doSomething(ParameterType a) {  
                return null;  
       }  
}

假如我有Super sup;
首先考虑参数,现在要调用sup.doSomething(o);
如果两个函数需要的参数类型一致,没有什么值得讨论的。假如在一些情况下,sub需要的参数类型变成了和ParameterType有继承关系的另一个类NewParameterType。如下:

class Sub extends Super {  
          ReturnType doSomething(NewParameterType a) {  
                return null;  
          }  
}

如果NewParameterType是ParameterType的子类,那么sup.doSomething(o)调用中,如果o是指向NewParameterType的对象,则不会被通过。只有NewParameterType是ParameterType的父类的时候,才不会发生任何问题。所以在提出逆变参数的概念。
同样地,利用这个原则也可以用来理解。
假如在某使用环境中有如下语句:

ReturnType rt = sup.doSomething(o);

假如设计变更,子类需要返回一个与ReturnType有继承关系的类NewReturnType,如果返回的是ReturnType的父类,那么则上面语句则不能通过编译,只有NewReturnType是ReturnType的子类的时候,才不会违反Liskov子类原则。

Java泛型
在Java泛型中,例如:

class DataHolder<T>{

}

假如类型 A ≤ B, 但是直接使用DataHolder<A> 和DataHolder<B>是不可变的,之前我们已经叙述过。
但是利用java提供的通配符语法,却可以提供一个协变的类型转换。
DataHolder<A> ≤DataHolder<? Extends B>

例如:

static boolean find(Iterable<? extends Object> where, Object what){  
        return false;  
}

可以使用Iterable<String> 来调用find函数。

参考资料:

http://stackoverflow.com/questions/2501023/demonstrate-covariance-and-contravariance-in-java

http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)

http://www.cnblogs.com/pugang/archive/2011/11/09/2242380.html

转载自:http://blog.csdn.net/hopeztm/article/details/8822545

http://www.2cto.com/kf/201304/205042.html

http://www.sjsjw.com/kf_code/article/102_31729_10353.asp

参考:http://msdn.microsoft.com/zh-cn/library/dd799517.aspx

http://www.cnblogs.com/pugang/archive/2011/11/09/2242380.html

http://arron-li.iteye.com/blog/673783

http://www.oschina.net/question/579073_120488

抱歉!评论已关闭.