设计模式之里氏替换原则
里氏替换原则(Liskov Substitution Principle,简称LSP): 子类可以替换父类
继承有一些优点:
1. 提高代码的重用性,子类拥有父类的方法和属性;
2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
缺点:侵入性、不够灵活、高耦合
1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成
非常糟糕的结果,要重构大量的代码。
任何基类可以出现的地方,子类一定可以出现。里氏替换原则是继承复用的基石,只有当衍生类可以替换基类,软件单位的功能不受到影响时,即基类随便怎么改动子类都不受此影响,那么基类才能真正被复用
因为继承带来的侵入性,增加了耦合性,也降低了代码灵活性,父类修改代码,子类也会受到影响,此时就需要里氏替换原则。
- 子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
a.子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法。
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
b.子类中可以增加自己特有的方法。
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
这是子类的新方法...
c.当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
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); } }
运行结果: 父类的运行结果:父类被执行... 子类替代父类后的运行结果:子类被执行... 在父类方法没有被重写的情况下,子方法被执行了,这样就引起了程序逻辑的混乱。
所以子类中方法的前置条件必须与父类中被覆写的方法的前置条件相同或者更宽松。不符合里式替换
d.当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
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=子类被执行...}
若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。
看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?
后果就是:你写的代码出问题的几率将会大大增加。
一步一个脚印,方便自己复习,欢迎大家指正,非常感谢,共同进步!
posted on 2019-01-24 16:26 lovebeauty 阅读(25405) 评论(5) 编辑 收藏 举报