一、里氏替换原则
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理
1.1 作用
- 里氏替换原则是实现开闭原则的重要方式之一
- 它克服了继承中重写父类造成的可复用性变差的缺点
- 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性
- 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险
1.2 实现方法
通俗理解:只要有父类出现的地方,都可以用子类来替代,里氏替换原则对继承进行了规则上的约束,有以下4个方面
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法
- 子类中可以增加自己特有的方法
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格
1.3 应用实例
子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法
⚠️ 如果子类覆写了父类中的非抽象方法会带来什么后果?
public class LSPtest {
public static void main(String[] args) {
Bird bird1 = new Swallow();
Bird bird2 = new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飞行300公里:");
try {
System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
//鸟类
class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子类
class Swallow extends Bird {}
//几维鸟类
class BrownKiwi extends Bird {
@Override
public void setSpeed(double speed) {
flySpeed = 0;
}
}
️ 上述实例代码中,BrownKiwi
没有翅膀不会飞,导致程序执行发生错误,违背了历史替换原则,正确做法应该是定义Bird
、BrownKiwi
的更一般的类animal
,Bird extends Animal
、 BrownKiwi extends Animal
、Swallow extends Bird
,并重新定义方法(例如动物都有runTime、runSpeed)。
子类中可以增加自己特有的方法
略
当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,这样才满足里氏替换原则
正确示例
public class A {
public void fun(HashMap map) {
System.out.println("父类被执行...");
}
}
class B extends A {
public void fun(Map map) {
System.out.println("子类被执行...");
}
}
// 子类的方法参数Map比父类的HashMap参数更宽松,满足里氏替换原则,只有父类的fun方法会被执行
class demo {
public static void main(String[] args) {
A a = new A();
HashMap<Integer, Integer> map = new HashMap();
a.fun(map); // 父类被执行...
B b = new B(); //父类存在的地方,可以用子类替代,子类B替代父类A
b.fun(map); // 父类被执行...
}
}
❓ 此时可能会有一个疑问,上述子类B代码中fun(Map map)
函数什么时候调用呢?
A a = new A();
Map<Integer, Integer> map = new HashMap();
B b = new B();
b.fun(map); // 子类被执行...
// 此时如果调用a.fun(map); 编译器会报错
错误示例
public class A {
public void fun(Map map) {
System.out.println("父类被执行...");
}
}
class B extends A {
public void fun(HashMap map) {
System.out.println("子类被执行...");
}
}
// 子类的方法参数Map比父类的HashMap参数更宽松,不满足里氏替换原则
// 两次调用fun(),分别执行了父类和子类的函数,引起了程序逻辑的混乱
class demo {
public static void main(String[] args) {
A a = new A();
HashMap<Integer, Integer> map = new HashMap();
a.fun(map); // 父类被执行...
B b = new B(); //父类存在的地方,可以用子类替代,子类B替代父类A
b.fun(map); // 子类被执行...
}
}
当子类的方法实现父类的(抽象)方法时,方法的后置条件(即方法的返回值)要与父类一致或更严格
public abstract class A {
public abstract Map fun();
}
class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put("b","子类被执行...");
return b;
}
}
class demo {
public static void main(String[] args){
A a=new B();
System.out.println(a.fun());
}
}
️ 里氏替换原则不是不能override,而是要按照父类所期望的那样去override。这告诉我们,继承也是可以使用的,不过要熟悉相关说明。