从开闭原则中可以看出面向对象设计的重要原则是创建抽象化,并且从抽象化导出具体化。具体化可以给出不同的版本,每一个版本都给出不同的实现。

    从抽象化到具体化的导出要使用继承关系和这里要引入的里氏代换原则,里氏代换原则由Barbara Liskov提出。

    在仔细研究之前,先看一看美猴王的智慧可以带给我们什么样的启发。

美猴王的智慧

    当年美猴王打到幽冥界,在生死簿寻找自己的姓名“直到那魂字一千三百五十号上,方注着孙悟空名字,乃天产石猴,该寿三百四十二岁...(悟空)饱掭浓墨...把猴属之类,但有名者,一概勾之...自此,山猴都有不老者,以阴司无名故也。”猴类是基类,石猴、猕猴是子类,子类的颜色与基类的颜色不同,如图所示:

    显然,幽冥地府管理一切生灵的生死的方法是以类来区分的。美猴王是一个石猴,而石猴类与猕猴类一样,都是猴类的子类,如图所示:

    因此,美猴王将猴类有姓名者统统消掉,显然是因为阴曹地府和勾魂的小鬼并不区分石猴类与猕猴类。

    换言之,猴类适用的,猕猴和石猴类全都适用,这其实就是里氏代换原则。

什么是里氏代换原则

    里氏代换原则的严格表达是:

    如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

    换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。

    比如,假设有两个类,一个是Base类,另一个是Derive类,并且Derived类是Base类的子类,那么一个方法如果可以接受一个基类对象b的话:

    method1(Base b)

那么它必然可以接受一个子类对象d,也即可以有method1(d)。

    里氏代换原则是继承复用的基石。只有当衍生类可以替换掉基类,软件单位的功能不会受到影响时,基类才能真正被复用,而衍生类也才能够在基类的基础上增加新的行为。

反过来代换不成立

    必须指出的是,反过来的代换则不成立,即如果一个软件实体使用的是一个子类的话,那么它不一定适用于基类。如果一个方法method2接收子类对象为参数的话:

    method2(Derived d)

那么一般而言不可以有method2(b)。

Java语言对里氏代换原则的支持

    在编译时期,Java语言编译器会检查一个程序是否符合里氏代换,当然这是一个无关实现的、纯语法意义上的检查。

    里氏代换要求凡是基类型使用的地方,子类型一定适用,因此子类型必须具备及类型的全部接口。或者说,子类型的接口必须包括全部的及类型的接口,而且还有可能更宽。如果一个Java程序破坏这一条件,Java编译器就会给出编译时期错误。

    举例而言,一个基类Base声明了一个public方法method(),那么其子类型Sub可否将这个方法的访问权限从public改换成package-private呢?换言之,子类型可否使用一个低访问权限的方法private method()将超类型的方法public method()置换(override)掉呢?

    从里氏代换的角度考察这个问题,就不难得出答案,因为客户端完全有可能调用超类型的公开方法,如果以子类型代之,这个方法却变成了私有的,客户端不能调用。显然这是违反里氏代换法则的,Java编译器根本就不会让这样的程序过关。

Java语言对里氏代换支持的局限

    Java编译器的检查是有局限的。为什么呢?举例来说,描写一个物体大小的量有精度和准确度两种属性。所谓精度,就是这个量的有效数字有多少位;而所谓准确度,是这个量与真实的物体大小相符合到什么程度。一个量可以有很高的精度,但是却无法与真实物体的情况相吻合。Java语言编译器所能够检查的仅仅是相当于精度的属性而已,它无法检查这个量与真实物体的差距。

    换言之,Java编译器不能检查一个系统在实现和商业逻辑上是否满足里氏代换法则。一个著名的例子,就是“正方形类否是长方形类的子类”的问题。