里氏替换原则LSP(Liskov Subsituation Principle)
里氏替换原则定义
所有父类出现的地方可以使用子类替换并不会出现错误或异常,但是反之子类出现的地方不一定能用父类替换。
LSP的四层含义
- 子类必须完全实现父类的方法
- 子类可以自己的个性(属性和方法)
- 覆盖或实现父类的方法时输入参数可以被放大
- 覆盖或实现父类的方法时输出结果可以被缩小
LSP的定义含义1——子类必须完全实现父类的方法
假设如下场景:定义一个枪支抽象类,一个场景类,三个枪支实现类,一个士兵类。此处,三个枪支完全实现了父类的方法。
抽象枪支类:射击功能
package des.lsp;
/**
* 抽象类 枪支
*/
abstract class AbstractGun {
//射击功能
public abstract void shoot();
}
子类实现
package des.lsp;
/**
* 手枪
*/
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.print("手枪可以射击");
}
}
package des.lsp;
/**
* 手枪
*/
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.print("步枪可以射击");
}
}
package des.lsp;
/**
* 步枪
*/
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.print("步枪可以射击");
}
}
士兵类:士兵类使用的是抽象枪支类,具体的需要在场景类中指定。
package des.lsp;
public class Soldier {
private AbstractGun gun;
public void setGun(AbstractGun _gun){
this.gun = _gun;
};
public void killEnemy(){
System.out.print("士兵开始杀人...");
gun.shoot();
}
}
场景类
package des.lsp;
public class Client {
public static void main(String[] args) {
// write your code here
Soldier s = new Soldier();
s.setGun(new Rifle());
s.killEnemy();
}
}
如果加入一个玩具枪类,即玩具枪类同样继承抽象枪支类,此时就会存在子类不能实现枪支类方法的情况,因为玩具枪和枪最本质的区别是玩具枪不能射击的,是无法杀死人的。但是,玩具枪的其他属性,比如颜色等一些属性可以委托抽象枪支类进行处理。
玩具枪继承枪支抽象类的情况:射击方法不能被实现,如果实现里面具体逻辑为空则毫无意义,即正常情况下不能实现父类的shoot方法,shoot方法必须去掉,从LSP来看如果去掉,则违背了LSP的第一个原则:子类必须实现父类方法。(代码层面来看如果去掉则会报错)
package des.lsp;
public class ToyGun extends AbstractGun {
@Override
public void shoot() {
//此方法不能实现,玩具枪不能射击
}
}
解决方法:单独建立一个抽象类玩具类,把与枪支共有的如声音、颜色交给抽象枪支类处理,而玩具枪所特有的玩具类的属性交给抽象玩具类处理,玩具枪类实现玩具抽象类。
LSP的定义含义2——子类可以含有自己的特性
如图引入,步枪的实现类即步枪由不同的型号。AUG:狙击枪可以由望远镜功能zoomOut方法。
此处Snipper是狙击手类,狙击手与狙击枪是密不可分,属于组合关系,所以狙击手类直接使用子类AUG。
package des.lsp;
//狙击枪
public class AUG extends Rifle {
//狙击枪特有功能
public void zoomOut(){
System.out.print("通过望远镜观察敌人...");
}
@Override
public void shoot() {
System.out.print("AUG射击敌人...");
}
}
package des.lsp;
//狙击手
public class Snipper {
//此处传入参数为子类,组合关系
public void killEnemy(AUG aug){
//观察
aug.zoomOut();
//射击
aug.shoot();
}
}
package des.lsp;
public class Client {
public static void main(String[] args) {
Snipper s = new Snipper();
s.killEnemy(new AUG());
}
}
LSP原则:父类不一定能替换子类
package des.lsp;
public class Client {
public static void main(String[] args) {
// write your code here
// Soldier s = new Soldier();
// s.setGun(new Rifle());
// s.killEnemy();
Snipper s = new Snipper();
s.killEnemy((AUG) new Rifle());//此处用父类代替了子类
}
}
报错代码
LSP的定义含义3——覆盖或实现父类方法时输入参数可以被放大
假设有如下场景
父类:方法入参<子类方法入参
场景类调用:父类调用自己方法。
package des.lsp;
import java.util.HashMap;
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();
}
}
输出结果
使用里氏替换原则:把所有父类出现的地方替换为子类
package des.lsp;
import java.util.HashMap;
public class Client {
public static void invoker(){
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
public static void main(String[] args) {
emy((AUG) new Rifle());
invoker();
}
}
输出结果
我们的本意是调用子类重载的方法,入参为Map的方法,但实际程序执行是调用的从父类继承的方法。如果子类的方法中入参的范围大于父类入参的范围,则子类代替父类的时候,子类的方法永远不会执行。
从另外角度来看,假如父类入参的范围大于子类的入参的范围,则父类替换子类就未必能存在,这时候很可能会调用子类的方法执行。此句话较为抽象,实际情况如下。
父类和子类的代码如下
public class Father {
public Collection doSomething(Map map){
System.out.print("父类被执行...");
return map.values();
}
}
public class Son extends Father {
public Collection doSomething(HashMap map) {
System.out.print("子类执行...");
return map.values();
}
}
场景类:调用父类
package des.lsp;
import java.util.HashMap;
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();
}
}
运行结果:不言而喻,是父类被执行
采用LSP后
package des.lsp;
import java.util.HashMap;
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的方法,但是程序执行的时候却自动为我们执行了子类的方法,此时就会导致混乱。
结论:子类中的方法的输入参数(前置条件或称形式参数)必须与父类中的输入参数一致或者更宽松(范围更大)。
LSP的定义含义4——覆盖或实现父类的方法时输出结果可以被缩小
理解:父类的返回类型为T,子类的返回类型为S,即LSP要求S<= T。
此时分为两种情况
- 如果时覆写,子类继承父类,继承的方法的入参必然相同,此时传入参数必须时相同或小于,返回的值必然不能大于父类返回值,这是覆写的要求。
- 如果时重载,这时候要求子类重载方法的参数类型或数量不相同,其实就是保证输入参数宽于或等于父类输入参数,这时候就保证了子类的方法永远不会被执行,其实就是含义3。
LSP的目的及理解
- 增强程序的健壮性
- 保证即使增加子类,原有的子类仍然可以继续运行。
- 从一方面来说,在程序中尽量避免直接使用子类的个性,而是通过父类一步一步的使用子类,否则直接使用子类其实就相当于直接把子类当作父类,这就直接导致父类毫无用途,父类和子类的关系也会显得没有必要存在了。