里氏替换原则
什么是里氏替换原则呢?它有两种定义:
第一种定义:如果对每一种类型为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的子类。
采用里氏替换原则的目的是增强程序的兼容性。
浙公网安备 33010602011771号