里氏替换原则

什么是里氏替换原则呢?它有两种定义:

第一种定义:如果对每一种类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换o2时,程序的行为没有发生任何改变,那么类型S是类型T的子类。

第二种定义:所有能使用基类的地方都必须能透明的使用其子类的对象

通俗点讲,就是父类能出现的地方子类也能出现

里氏替换原则四层含义:

1、子类必须完全实现父类的所有方法。

  我们在做系统设计时经常会定义一个接口或者抽象类,然后编码实现,调用类则直接传入接口或者抽象类,这其实就是使用了里氏替换原则。


 

代码:

//枪支的抽象类
public abstract class AbstractGun{
    public abstract void shoot();
}    

//手枪、步枪、机枪的实现类

public class Handgun extends AbstractGun{
    @Override
    public void shoot(){
          System.out.println("手枪射击...");
    }
}    


public class Rifleextends AbstractGun{
    @Override
    public void shoot(){
          System.out.println("步枪射击...");
    }
}    
    

public class MachineGun extends AbstractGun{
    @Override
    public void shoot(){
          System.out.println("机枪射击...");
    }
}    


//士兵的实现类
public class Soldier{
     //士兵的枪
     private AbstractGun gun;
     
    public void setGun(AbstractGun  _gun){
        gun = _gun;
    }
     
    piblic void killEnemy(){
      System.out.println("士兵开始杀敌人..."); gun.shoot(); } } //场景类 public class Client { public static void main(String[] args) { //产生三毛这个士兵 Soldier sanMao = new Soldier(); //给三毛一支枪 sanMao.setGun(new Rifle()); sanMao.killEnemy(); } }

类中调用其他类时请务必使用父类或者接口,如果不能使用父类或者接口则说明类的设计违背了里氏替换原则(LSP)

 如果我们有一个玩具手枪,该如何定义呢如果我们有一个玩具手枪,该如何定义呢?

玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。

在这种情况下,我们发现业务调用类已经出现了问题,正常的业务逻辑已经不能运行,有两种解决办法:

1>在Solider类中新增instanceof判断,如果是玩具枪,则不能杀人。但是没增加一个类,所有与枪类有关的类都必须修改,因此不可采取。

2>ToyGun脱离枪类单独成为一个父类

注意 如果父类的某些方法在子类中已经发生“畸变” ,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承

2、子类可以有自己的个性

子类可以有自己的属性和方法。里氏替换原则可以正着使用,但是不能反过来使用,也就是说,子类出现的地方,父类未必能替换。

 AUG继承了Rifle类,狙击手(Snipper)则直接使用AUG狙击步枪

 代码如下:

 

//AUG狙击枪源码代码
public class AUG extends Rifle {
    //狙击枪都携带一个精准的望远镜
  
public void zoomOut(){
    System.out.println("通过望远镜察看敌人...");
  }

   public void shoot(){
    System.out.println("AUG射击...");
   }

} 

//AUG狙击手类的源码代码
public class Snipper {
  public void killEnemy(AUG aug){
    //首先看看敌人的情况,别杀死敌人,自己也被人干掉
    aug.zoomOut();
    //开始射击
    aug.shoot();
  }

}

 //业务场景Client类

public class Client {
  public static void main(String[] args) {
    //产生三毛这个狙击手
    Snipper sanMao = new Snipper();
    sanMao.setRifle(new AUG());
    sanMao.killEnemy();

  }
}

 在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击。这个时候,我们能不能直接使用父类传递进来呢?

 //业务场景Client类
public class Client {
  public static void main(String[] args) {
    //产生三毛这个狙击手
    Snipper sanMao = new Snipper();
    sanMao.setRifle((AUG)(new Rifle()));
    sanMao.killEnemy();
  }
}

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型(downcast)是不安全的从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

3、覆盖或者实现父类方法时输入参数可以被放大

 


 

父类参数范围小于子类参数范围

 

//Father类源代码
public class Father {
  public Collection doSomething(HashMap map){
    System.out.println("父类被执行...");
    return map.values();
  }
}

 

这个类非常简单,就是把HashMap转换为Collection集合类型

//子类源代码
public class Son extends Father {
  //放大输入参数类型
  public Collection doSomething(Map map){
    System.out.println("子类被执行...");
    return map.values();
  }
}

方法名虽然相同,但方法的输入参数不同,重载(Overload)!

 

//场景类源代码
public class Client { 
  public static void invoker(){
    //父类存在的地方,子类就应该能够存在
    Father f = new Father();
    HashMap map = new HashMap();
    f.doSomething(map);
  }
  public static void main(String[] args) {
    invoker();
  } }

 

父类被执行..

根据里氏替换原则,父类出现的地方子类就可以出现

 

//子类替换父类后的源代码
public class Client {
  public static void invoker(){
    //父类存在的地方,子类就应该能够存在
    Son f =new Son();
    HashMap map = new HashMap();
    f.doSomething(map);
  }
  public static void main(String[] args) {
    invoker();
  }
}

 

父类被执行..

运行结果还是一样,父类方法的输入参数是HashMap类型,子类的输入参数是Map类型,也就是说子类的输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远都不会被执行。

 


 

父类参数范围大于子类参数范围

 

//Father类源代码
public class Father {
  public Collection doSomething(Map map){
    System.out.println("父类被执行...");
    return map.values();
  }
}

 

 

 

//子类源代码
public class Son extends Father {
  //放大输入参数类型
  public Collection doSomething(HashMap map){
    System.out.println("子类被执行...");
    return map.values();
  }
}

 

 

//场景类
public
class Client {   public static void invoker(){     //有父类的地方就有子类     Father f= new Father();     HashMap map = new HashMap();     f.doSomething(map);   }
  
public static void main(String[] args) {   invoker();   } }

父类被执行...

 4、覆写或者实现父类方法时输出结果可以被缩小

  父类的一个方法的返回值是一个类型T,子类的相同方法(重载或者覆写)的返回值为S,那么里氏替换原则就要求S必须小于T,即S和T是同一个类型或者S是T的子类。

采用里氏替换原则的目的是增强程序的兼容性。

posted on 2017-07-15 16:25  sky950506  阅读(168)  评论(0)    收藏  举报

导航