里氏替换原则是运用于继承之上,为了克服其缺点而来的
1. 里氏替换原则的定义
-
里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
-
里氏代换原则告诉我们,在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。
-
里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
2. 里氏替换原则出现原因及其内容
继承有一些优点:
- 提高代码的重用性,子类拥有父类的方法和属性;
- 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
缺点:侵入性、不够灵活、高耦合
- 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
- 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
所以说,继承有优点,也有很大的缺点,为了避免这些缺点给我们带来的影响,我们就可以采用里氏替换原则来减小甚至消除影响
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
3. 举例说明里氏替换原则四个点
3.1 第一点
-子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法
在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。子类可以实现父类的抽象方法很好理解,事实上,子类也必须完全实现父类的抽象方法,哪怕写一个空方法,否则会编译报错。
里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
@Override
public void fun(int a,int b){
System.out.println(a+"-"+b+"="+(a-b));
}
}
public class demo {
public static void main(String[] args){
System.out.println("父类的运行结果");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b=new B();
b.fun(1,2);
}
}
运行结果:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1
3.2 第二点
-子类中可以增加自己特有的方法。
在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法
public class A {
public void fun(int a,int b){
System.out.println(a+"+"+b+"="+(a+b));
}
}
public class B extends A{
public void newFun(){
System.out.println("这是子类的新方法...");
}
}
public class demo {
public static void main(String[] args){
System.out.print("父类的运行结果:");
A a=new A();
a.fun(1,2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
B b=new B();
b.fun(1,2);
//子类B的新方法
b.newFun();
}
}
运行结果:
父类的运行结果:1+2=3
子类替代父类后的运行结果:1+2=3
这是子类的新方法…
3.3 第三点
-当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
为什么呢?因为子类输入参数如果比父类严格的话,就会出现想调用父类方法缺调用了子类方法这个事情的发送,是错误的
public class LSP {
class A {
public void fun(HashMap map){
System.out.println("父类被执行...");
}
}
class B extends A{
public void fun(Map map){
System.out.println("子类被执行...");
}
}
public static void main(String[] args){
System.out.print("父类的运行结果:");
LSP lsp =new LSP();
LSP.A a= lsp.new A();
HashMap<Object, Object> map=new HashMap<Object, Object>();
a.fun(map);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
LSP.B b=lsp.new B();
b.fun(map);
}
}
运行结果:
父类的运行结果:父类被执行…
子类替代父类后的运行结果:父类被执行…
符合条件
我们应当注意,子类并非重写了父类的方法,而是重载了父类的方法。因为子类和父类的方法的输入参数是不同的。
子类方法的参数Map比父类方法的参数HashMap的范围要大,所以当参数输入为HashMap类型时,只会执行父类的方法,不会执行父类的重载方法。这符合里氏替换原则。
如果不这样会是什么情况呢?
//将子类方法的参数范围缩小会怎样?
import java.util.Map;
public class A {
public void fun(Map map){
System.out.println("父类被执行...");
}
}
import java.util.HashMap;
public class B extends A{
public void fun(HashMap map){
System.out.println("子类被执行...");
}
}
import java.util.HashMap;
public class demo {
static void main(String[] args){
System.out.print("父类的运行结果:");
A a=new A();
HashMap map=new HashMap();
a.fun(map);
//父类存在的地方,都可以用子类替代
//子类B替代父类A
System.out.print("子类替代父类后的运行结果:");
B b=new B();
b.fun(map);
}
}
运行结果:
父类的运行结果:父类被执行…
子类替代父类后的运行结果:子类被执行…
在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。
所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。不符合里式替换
3.4 第四点
-当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
public class LSP1 {
abstract class A {
public abstract Map fun();
}
class B extends A{
@Override
public HashMap fun(){
HashMap b=new HashMap();
b.put("b","子类被执行...");
return b;
}
}
public static void main(String[] args){
LSP1 lsp =new LSP1();
LSP1.A a=lsp.new B();
System.out.println(a.fun());
}
}
运行结果:
{b=子类被执行…}
若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。
比如:
所以,是不可以这样做的
4. 总结
此原则的第一点第二点特别重要,实在需要覆盖父类方法时,就要遵守第三点第四点