面向对象设计原则之里氏代换原则
一、什么是里氏代换原则
- 严格的定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1代换o2时,程序P的行为没有变化,那么类型S是类型T的子类型。
- 通俗的定义:所有引用基类的地方必须能透明地使用其子类的对象。
- 更通俗的定义:子类可以扩展父类的功能,但不能改变父类原有的功能。
二、里氏代换原则包含4层含义
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
2.1、子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。子类可以实现父类的抽象方法很好理解,事实上,子类也必须完全实现父类的抽象方法,哪怕写一个空方法,否则会编译报错。
里氏代换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏代换原则就是表达了这一层含义。
在面向对象的设计思想中,继承这一特性为系统的设计带来了极大的便利性,但是由之而来的也潜在着一些风险。就像上面所提到的那一场景一样,对于那种情况最好遵循里氏代换原则,类B继承类A时,可以添加新方法完成新增功能,尽量不要重写父类A的方法。否则可能带来难以预料的风险:
public class A {
public int add(int a,int b){
return a+b;
}
}
public class B extends A {
@Override
public int add(int a,int b){
return a-b;
}
}
public class Test {
public static void main(String[] args) {
A a = new B();
System.out.println("50+10="+a.add(50,10));//运行结果:50+10=40
}
}
上面的运行结果50+10=40
明显是错误的。类B继承了类A,后来需要增加新功能,类B并没有新写一个方法,而是直接重写了父类A的add方法,违背里氏代换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错。
2.2、子类可以增加自己特有的方法
在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其符合里氏代换原则,代码如下:
public class A {
public int add(int a,int b){
return a+b;
}
}
public class B extends A {
public int subtract(int a,int b){
return a-b;
}
}
public class Test {
public static void main(String[] args) {
A a = new B();
System.out.println("50+10="+a.add(50,10));//运行结果:50+10=60
System.out.println("50-10="+((B) a).subtract(50,10));//运行结果:50-10=40
}
}
2.3、当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
public class Father {
public void func(HashMap map){
System.out.println("执行父类!!!");
}
}
public class Son extends Father {
public void func(Map map){//方法的形参比父类的更宽松
System.out.println("执行子类!!!");
}
}
public class Test {
public static void main(String[] args) {
Father father = new Son();//引用基类的地方能透明地使用其子类的对象。
HashMap h = new HashMap();
father.func(h);//运行结果:执行父类!!!
}
}
注意Son类的func方法前面是不能加@Override注解的,因为否则会编译提示报错,因为这并不是重写(Override),而是重载(Overload),因为方法的输入参数不同。
重写和重载的区别:
重载(Overloading)
- 方法重载是让类以统一的方式处理不同数据类型的手段。
- 一个类中可以创建多个方法,它们具有相同的名字,但具有不同的参数和不同的定义。调用方法时通过传递给它们的不同参数个数和参数类型来决定具体使用哪个方法。
- 返回值类型可以相同也可以不相同,无法以返回型别作为重载函数的区分标准。
重写(Overriding)
- 子类对父类的方法进行重新编写。如果在子类中的方法与其父类有相同的的方法名、返回类型和参数列表,我们说该方法被重写 (Overriding)。
- 如需父类中原有的方法,可使用super关键字,该关键字引用了当前类的父类。
- 子类函数的访问修饰权限不能低于父类的。
2.4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
public abstract class AbstractFather {
public abstract Map func();
}
public class ASon extends AbstractFather {
@Override
public HashMap func() {//方法的返回值比父类的更严格
HashMap h = new HashMap();
h.put("h", "执行子类...");
return h;
}
}
public class Test {
public static void main(String[] args) {
AbstractFather father = new ASon();//引用基类的地方能透明地使用其子类的对象。
System.out.println(father.func());//运行结果:{h=执行子类...}
}
}
三、总结
继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了一些弊端,它增加了对象之间的耦合性。因此在系统设计时,遵循里氏替换原则,尽量避免子类重写父类的方法,可以有效降低代码出错的可能性。