里氏替换原则


  前言:今天是3.15,一个特殊的日子。不知道还会曝光出多少家不良企业,更不知道潜藏的未被曝光的企业数量之巨有没有超出我的想象力。每年都会爆出一些诸如“塑化剂、毒胶囊、问题奶、速成鸡”等等新的食品安全关键词,走进餐馆,走进食堂,走进超市,还真不知道什么东西敢碰。新的问题一年一年曝光,却一年比一年严重。不良商家究竟还有没有底线?说好的节操呢?

设计模式系列文章

1、问题的由来

  我们都知道面向对象有三大特性:封装、继承、多态。所以我们在实际开发过程中,子类在继承父类后,根据多态的特性,可能是图一时方便,经常任意重写父类的方法,那么这种方式会大大增加代码出问题的几率。比如下面场景:类C实现了某项功能F1。现在需要对功能F1作修改扩展,将功能F1扩展为F,其中F由原有的功能F1和新功能F2组成。新功能F由类C的子类C1来完成,则子类C1在完成功能F的同时,有可能会导致类C的原功能F1发生故障。这时候里氏替换原则就闪亮登场了。

2、什么是里氏替换原则

  前面说过的单一职责原则,从字面意思就很好理解,但是里氏替换原则就有点让人摸不着头脑。查过资料后发现原来这项原则最早是在1988年,由麻省理工学院一位姓里的女士(Liskov)提出来的。

  英文缩写:LSP (Liskov Substitution Principle)。

  严格的定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。 

  通俗的定义:所有引用基类的地方必须能透明地使用其子类的对象。

  更通俗的定义:子类可以扩展父类的功能,但不能改变父类原有的功能。

  代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//抽象父类电脑
public  abstract  class  Computer {
     public  abstract  void  use();
}
 
class  IBM extends  Computer{
     @Override
     public  void  use() {
         System.out.println( "use IBM Computer." );
     }
}
 
class  HP extends  Computer{
     @Override
     public  void  use() {
         System.out.println( "use HP Computer." );
     }
}
 
public  class  Client{
     public  static  void  main(String[] args) {
         Computer ibm = new  IBM();
         Computer hp = new  HP(); //引用基类的地方能透明地使用其子类的对象。
         
         ibm.use();
         hp.use();
     }
}

3、四层含义

  里氏替换原则包含以下4层含义:

  • 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

  现在我们可以对以上四层含义逐个讲解。

  子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法

  在我们做系统设计时,经常会设计接口或抽象类,然后由子类来实现抽象方法,这里使用的其实就是里氏替换原则。子类可以实现父类的抽象方法很好理解,事实上,子类也必须完全实现父类的抽象方法,哪怕写一个空方法,否则会编译报错。

  里氏替换原则的关键点在于不能覆盖父类的非抽象方法。父类中凡是已经实现好的方法,实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些规范,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

  在面向对象的设计思想中,继承这一特性为系统的设计带来了极大的便利性,但是由之而来的也潜在着一些风险。就像开篇所提到的那一场景一样,对于那种情况最好遵循里氏替换原则,类C1继承类C时,可以添加新方法完成新增功能,尽量不要重写父类C的方法。否则可能带来难以预料的风险,比如下面一个简单的例子还原开篇的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public  class  C {
     public  int  func( int  a, int  b){
         return  a+b;
     }
}
 
public  class  C1 extends  C{
     @Override
     public  int  func( int  a, int  b) {
         return  a-b;
     }
}
 
public  class  Client{
     public  static  void  main(String[] args) {
         C c = new  C1();
         System.out.println( "2+1="  + c.func( 2 , 1 ));
     }
}

  运行结果:2+1=1

  上面的运行结果明显是错误的。类C1继承C,后来需要增加新功能,类C1并没有新写一个方法,而是直接重写了父类C的func方法,违背里氏替换原则,引用父类的地方并不能透明的使用子类的对象,导致运行结果出错。

  子类中可以增加自己特有的方法

  在继承父类属性和方法的同时,每个子类也都可以有自己的个性,在父类的基础上扩展自己的功能。前面其实已经提到,当功能扩展时,子类尽量不要重写父类的方法,而是另写一个方法,所以对上面的代码加以更改,使其符合里氏替换原则,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public  class  C {
     public  int  func( int  a, int  b){
         return  a+b;
     }
}
 
public  class  C1 extends  C{
     public  int  func2( int  a, int  b) {
         return  a-b;
     }
}
 
public  class  Client{
     public  static  void  main(String[] args) {
         C1 c = new  C1();
         System.out.println( "2-1="  + c.func2( 2 , 1 ));
     }
}

  运行结果:2-1=1

  当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松

  代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import  java.util.HashMap;
public  class  Father {
     public  void  func(HashMap m){
         System.out.println( "执行父类..." );
     }
}
 
import  java.util.Map;
public  class  Son extends  Father{
     public  void  func(Map m){ //方法的形参比父类的更宽松
         System.out.println( "执行子类..." );
     }
}
 
import  java.util.HashMap;
public  class  Client{
     public  static  void  main(String[] args) {
         Father f = new  Son(); //引用基类的地方能透明地使用其子类的对象。
         HashMap h = new  HashMap();
         f.func(h);
     }
}

  运行结果:执行父类...

  注意Son类的func方法前面是不能加@Override注解的,因为否则会编译提示报错,因为这并不是重写(Override),而是重载(Overload),因为方法的输入参数不同。重写和重载的区别在Java面向对象详解一文中已作解释,此处不再赘述。

  当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格

  代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import  java.util.Map;
public  abstract  class  Father {
     public  abstract  Map func();
}
 
import  java.util.HashMap;
public  class  Son extends  Father{
     
     @Override
     public  HashMap func(){ //方法的返回值比父类的更严格
         HashMap h = new  HashMap();
         h.put( "h" , "执行子类..." );
         return  h;
     }
}
 
public  class  Client{
     public  static  void  main(String[] args) {
         Father f = new  Son(); //引用基类的地方能透明地使用其子类的对象。
         System.out.println(f.func());
     }
}

  执行结果:{h=执行子类...}

4、总结

  继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了一些弊端,它增加了对象之间的耦合性。因此在系统设计时,遵循里氏替换原则,尽量避免子类重写父类的方法,可以有效降低代码出错的可能性。

转自:http://www.cnblogs.com/hellojava/archive/2013/03/15/2960905.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值