目录
前言
这是我对于Liskov原则的定义和相关概念,以及我自己的理解进行的概述。
定义
Liskov原则是一个关于设计父类和子类关系的原则。
由Barbara Liskov的话来定义该原则:
如果对于类型T的对象x,q(x)成立,那么对于类型T的子类型S的对象y,q(y)也成立。
这个定义比较抽象,我们可以在Java中的上下文中来尝试理解它。
Java中的Liskov原则
在Java中,Liskov原则可以理解为以下一系列要求:
1.某一类型T的子类型S可以增加新方法,但是不能删除类型T的方法。
2.子类型T需要实现抽象类型中未实现的方法。抽象类型可以指的是类型T实现的接口(implements),也可以指的是一个抽象类(extends)。
3.子类型中重写的方法的返回值类型必须与父类/接口的返回值类型相同或者是它的子类型,即协变(co-variant)的类型。---对重写方法返回值类型的要求。
4.子类型重写方法的参数类型要么相同要么是逆变(反协变)的,对于后者java会认为是重载方法而非重写方法。---对重写方法参数类型的要求。
5.子类型中不能抛出额外的异常。
6.对于这些子类型/它们重写的方法的规约,要求更强或相同的不变量、更弱或不变的前置条件、更强或不变的后置条件。
对于协变(co-variant)
指的是父类和子类中对应方法的对应返回值类型/异常类型的一种变化:即由父类到子类,越来越具体,对应的返回值类型/异常类型也越来越具体。如下:
子类中对应方法的返回值类型变成String(Object是它的超类),更加“具体”了。
子类型抛出的异常更加“具体”,IOException是Throwable的子类。
对于逆变(contravariance)
从父类型到子类型,越来越具体,但是对应方法的参数类型越来越抽象。
注意,从父类到子类,参数类型是可以协变的,但是这样是不安全的,因为协变的参数类型意味着,随着类的具体化,对应方法的输入更加“窄”了,这是不合理的。
可以看到,子类型的参数类型是Object(String的超类),更加“具体"了。
但是要注意的的是这个子类”重写“的方法上加了一个Override标签,这会导致编译错误,因为java要求重写方法的签名必须和父类是完全一致的!也就是说java并不支持重写方法中参数类型的逆变,去掉Override标签之后,Java会将该方法视作对子类继承自父类方法的重载。(也就是说,如果父类还没有实现这个方法,你还得去实现它)。
总结
我们可以用下面这张图作为对以上内容的总结:
T'指向T意味着T'类是T类的子类。
在子类的实现中,比较保守的方法是第二种,所有方法的返回值类型和参数类型都和父类的一致。
注意到Java支持子类重写方法中返回值类型的协变,但是并不支持重写方法中参数类型的逆变或者协变,后者甚至是不安全的,因为这会带来更具体的实现、但更窄的输入。
当我们声明变量用的是父类,而实例类型是它的子类时,协变的参数类型会带来潜在的风险,因为我们在把它当作父类来使用。
以上就是我对于Liskov原则及相关概念的概述。