1. 协变与逆变
协变与逆变针对的是带类型参数的类型,例如List[T]
2. 协变
对一个带类型参数的类型,比如List[T],如果对A及其子类型B,满足List[B]是List[A]的子类型,那么称为covariance协变
trait A[+T] // 定义A为协变类型
class X // 定义具体类型X
class Y extends X // 定义具体类型Y,且Y是X的子类
val x:A[X] = new A[Y]{} // 因为Y是X的子类,因此A[Y]也是A[X]的子类,根据里氏替换原则,A[X]类型的变量x可以接受一个A[Y]类型实例
3. 逆变
对一个带类型参数的类型,比如List[T],如果对A及其子类型B,满足List[B]是List[A]的父类型,那么称为contravariance逆变
trait A[-T] // 定义A为逆变类型
class X // 定义具体类型X
class Y extends X // 定义具体类型Y,且Y是X的子类
val x: A[Y] = new A[X]{} // 因为Y是X的子类,因此A[Y]是A[X]的父类,根据历史替换原则,A[Y]类型的变量x可以接受一个A[X]类型实例
4. 可变类型
如果一个带类型参数的类型支持协变或逆变,则称这个类型为variance可变的或变型
5. 不可变类型
如果一个带参数类型的类型不支持协变或逆变,则成这个类型为invariant不可变的,java中的泛型类型都是不可变的
例如:List并不是List的子类型
6. 协变类型的定义
trait List[+T] // scala写法,此时List[String]作为List[Any]的子类型
List<? extends Object> list = new ArrayList<String>(); // java写法,使用点变型(use-site variance),所谓使用点,是在声明变量时指定类型范围
val a: List[_ <: Any] = List[String]("A") // scala兼容java泛型通配符的形式,引入存在类型(extential type),支持使用点变型
7. 可变类型不可被继承
可变类型不会被继承,父类为可变类型,子类为不变类型,如果子类需要保持可变类型,仍然需要声明
trait A[+T] // 定义A为协变类型
class X // 定义一个具体类型X
class Y extends X // 定义一个具体类型Y,且Y是X的子类
class C[T] extends A[T] // 语法:C带参数T,A带参数T,且A的参数不带+号
val t: C[X] = new C[Y] // 此时C为不可变类型,报错:Note: Y <: X, but class C is invariant in type T.You may wish to define T as +T instead
// 子类若想为可变类型
class C[+T] extends A[T] // 语法:C带参数+T,A带参数T,且A的参数不带+号
val t: C[X] = new C[Y] // 此时C为协变类型,正常返回
8. 函数类型中的协变与逆变
8.1 函数写法
写法:小括号中为入参类型,最多可以有22个,最少为0,右箭头右侧为返回类型
(T1, T2, ...) => R
8.2 通配符
_表示任意类型
val x: String => _ = null
8.3 函数参数类型的可变性
8.3.1 入参类型都是逆变
// 入参类型如果是协变类型,会报错
class In[+A]{def fun(x: A){}} // 报错error: covariant type A occurs in contravariant position in type A of value x
// 入参类型如果是逆变类型,则正常
class In[-A]{def fun(x: A){}} // 正常
8.3.2 结果类型都是协变
// 结果类型如果是逆变类型,会报错
class In[-A]{def fun():A=null.asInstanceOf[A]} // 报错:error: contravariant type A occurs in covariant position in type ()A of method fun
// 结果类型如果是协变类型,则正常
class In[+A]{def fun():A = null.asInstanceOf[A]} // 正常
9 协变点和逆变点
9.1 入参类型是逆变点
为什么入参类型是逆变点?
假设有协变类型In[+A]
父类型In[AnyRef]中的方法fun(x: AnyRef)
子类型In[String]中的方法fun(x: String)
- 根据里氏替换原则,父类型对象都可以被子类型对象替换,使用In[String]替换In[AnyRef]后,fun(x: String)只能接受String类型入参
- 原程序上下文中提供的入参变量类型仍为AnyRef,会超出子类型对象函数fun(x: String)的处理范围
- 子类型函数fun的参数类型应当不小于父类型函数参数类型,才能满足里氏替换原则
- 因此In[AnyRef]应当为In[String]的子类,所有使用In[String]的场合才可以替换为In[AnyRef],并且根据已知,String是AnyRef子类,因此类型参数A为逆变的
- 总结:为了满足里氏替换原则,函数参数类型必须为逆变类型
9.2 结果类型是协变点
为什么结果类型是协变点
假设有逆变类型In[-A]
子类型In[AnyRef]中的方法fun():AnyRef
父类型In[String]中的方法fun():String
- 根据里氏替换原则,父类型对象都可以被子类型对象替换,使用In[AnyRef]替换In[String]后,fun():AnyRef只能返回AnyRef类型结果
- 原上下文中需要提供的结果变量仍为String,没办法接受子类型对象函数fun():AnyRef的返回结果
- 子类型对象函数fun():A的返回结果必须是String的子类,才能满足里氏替换原则
- 因此In[AnyRef]应当为In[String]的父类型,所有使用In[AnyRef]的场合才可以被替换为In[String],并且根据已知,AnyRef是String父类,因此参数A为协变的
- 总结:为了满足里氏替换原则,函数返回类型必须为协变类型
9.3 函数类型的里氏替换原则
假设有2个函数: f1, f2;
我们若能说f2是f1的子类,当且仅当:f2的定义域类型x2 “大于” f1的定义域类型x1,且f2的值域y2 “小于” f1的值域y1; 其中“大于”指“是父类”;“小于”指“是子类”;
进一步讲,f1(父类函数)能接受的任意参数,f2也能接受,且经由f2映射而得出的结果的类型,也一定在经由f1映射所得结果的类型的“范围内”(是其子类)
再进一步讲:当代码里有对f1的调用“f1(x)”时,你可以尽管将这里的f1换成f2而不会出现类型错误,这也就是关乎“函数类型”的“里氏替换原则(LSP)”