如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
看不懂,没关系,还有一种定义
所有引用基类的地方必须透明的使用其子类的对象
还是看不懂,没关系。
简单的来说,T1是一个基类,T2是T1的子类,所有使用T1的地方,都代替为T2之后,程序行为没有发生改变。这下应该懂了吧。
里氏替换原则其实就是为“良好的继承”制定一些规范。
1. 子类要完全实现父类的抽象方法,但尽量不要覆盖父类的非抽象方法
这一点很容易理解,如果子类覆盖了父类的非抽象方法,当使用子类代替父类时,程序行为可能会有所改变。
举个例子:
父类
public class Calculator {
public int add(int num1, int num2) {
return num1 + num2;
}
}
public class Client {
public static void main(String[] args) {
Calculator calculator = new Calculator();
calculator.add(10, 5);
// 结果为15
}
}
子类
public class MiniCalculator extends Calculator {
public int add(int num1, int num2) {
return num1 - num2;
}
}
public class Client {
public static void main(String[] args) {
// 子类代替父类
// Calculator calculator = new Calculator();
MiniCalculator calculator = new MiniCalculator();
calculator.add(10, 5);
// 结果为5
}
}
例子中,由于子类MiniCalculator重写了父类Calculator的add方法,当子类MiniCalculator代替父类Calculator时,导致计算结果有误,也就是程序的行为发生了变化。所以对父类的非抽象方法,尽量不要覆盖重写。
2.子类中可以增加自己特有的方法
子类一般都会有自己特有的属性或方法,这点是肯定的。
3.当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数要更抽象化。
当我们要覆盖重载父类非抽象方法时,为了确保父类的方法能被正确执行,方法的形参应该更抽象化。
举个例子:
父类
public class Father{
public Collection doSomething(HashMap map){
System.out.println("父类被执行");
return map.values();
}
}
子类
public class Son extends Father{
public Collection doSomething(Map map){
System.out.println("子类被执行");
return map.values();
}
}
调用父类执行:
public class Client {
public static void main(String[] args) {
Father father = new Father();
HashMap map = new HashMap();
father.doSomething(map);
// 结果是父类被执行
}
}
当我们用子类代替父类时:
public class Client {
public static void main(String[] args) {
// 子类代替父类
// Father father = new Father();
Son son = new Son();
HashMap map = new HashMap();
son.doSomething(map);
// 结果是还父类被执行
}
}
结果是一样的,由于传入的是一个HashMap类型,所以由父类去执行,子类重写等于没写,不知道这样的重写有啥意义。
4.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更具体化
父类的一个方法的返回值是一个类型T,子类的相同方法的返回值为S,那么里氏替换原则就要求S必须小于等于T。
其实上面的规范,一般人(除了不小心)都不会违反,因为那样做没有意义!!所以说了等于白说!
但是告诉了我们一个想法,如果我们遵守了里氏替换原则,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。这样的话,在一定程度上增强程序的健壮性。