在面向对象的语言中,继承是必不可少的。
里氏替换原则为良好的继承定义了一个规范,包括了四层含义,下面 一 一介绍。
举一个简单的例子来说明,很多人都玩过刀塔传奇,是一款卡牌的游戏,简单描述一下里面的武器。如类图:
第一种含义:子类必须完全实现父类的方法
武器的主要职责就是用来打BOSS,如何攻击在各个子类中有具体的定义,刀和斧头是攻击距离比较近的,魔杖属于远程攻击。
在卡牌(Hero)这个类中定义了使用什么武器的方法,用武器打BOSS的方法,具体用什么武器打BOSS,调用的时候才知道,示例代码如下:
AbstractWeapon:
package com.lsp;
public abstract class AbstractWeapon {
public abstract void attck();
}
Axe:
package com.lsp;
public class Axe extends AbstractWeapon {
@Override
public void attck() {
System.out.println("用斧头在攻击。。。。");
}
}
Knife:
package com.lsp;
public class Knife extends AbstractWeapon {
@Override
public void attck() {
System.out.println("用刀在攻击。。。。");
}
}
MagicCane:
package com.lsp;
public class MagicCane extends AbstractWeapon {
@Override
public void attck() {
System.out.println("用魔杖在攻击。。。。");
}
}
Hero:
package com.lsp;
public class Hero {
// 定义一种武器
private AbstractWeapon weapon;
// 带上一种武器
public void setWeapon(AbstractWeapon weapon) {
this.weapon = weapon;
}
public void kill() {
System.out.println("正在打BOSS。。。。");
weapon.attck();
}
}
Client:
package com.lsp;
public class Client {
public static void main(String[] args) {
// 在背包里的斧头
Axe axe = new Axe();
// 我喜欢用斧王这个英雄,他的大招好用
Hero axeKing = new Hero();
//给斧王用这个武器
axeKing.setWeapon(axe);
//斧王用斧头打BOSS
axeKing.kill();
}
}
运行结果:
正在打BOSS。。。。
用斧头在攻击。。。。
子类可以有自己的行为和外观,也就是方法和属性。
CS,反恐精英,是一款超级经典游戏,下面拿里面的枪举例。我们都知道枪的基本职责就是射击,定义一个父类(AbstractGun),先把枪大致分为三种,机枪(MachineGun),步枪(Rifle),手枪(HandGun)。如类图:
SnipeGun,是阻击枪。这种枪新增了一个自己的方法zoom( 瞄准)。
现在模拟阻击手拿着阻击枪瞄准射击,示例如下:
AbstractGun:
package com.lsp;
public abstract class AbstractGun {
public abstract void shut();
}
MechineGun:
package com.lsp;
public class MechineGun extends AbstractGun {
@Override
public void shut() {
System.out.println("正在用机枪射击。。。");
}
}
Rifle:
package com.lsp;
public class Rifle extends AbstractGun {
@Override
public void shut() {
System.err.println("用步枪射击。。。");
}
}
HandGun:
package com.lsp;
public class HandGun extends AbstractGun {
@Override
public void shut() {
System.out.println("用手枪射击。。。。");
}
}
SnipeGun:
package com.lsp;
public class SnipeGun extends Rifle {
public void zoom() {
System.out.println("正在用阻击手瞄准目标。。。");
}
public void shut() {
System.err.println("阻击枪射击。。。。");
}
}
Sniper:
package com.lsp;
public class Sniper {
private SnipeGun gun;
public void setGun(SnipeGun gun) {
this.gun = gun;
}
public void kill() {
gun.zoom();
gun.shut();
}
}
Client:
package com.lsp;
public class Client {
public static void main(String args[]) {
// 产生奥巴麻这个阻击手
Sniper obama = new Sniper();
// 给奥巴麻配把阻击柴油机
obama.setGun(new SnipeGun());
// 奥巴麻进入战斗
obama.kill();
}
}
输出结果:
正在用阻击手瞄准目标。。。
阻击枪射击。。。。
第三种含义:覆盖或实现父类方法时输入参数可以被 放大
先看例子:
father:
package com.lsp;
import java.util.Collection;
import java.util.HashMap;
public class Father {
public Collection toMap(HashMap map) {
System.out.println("父类被执行....");
return map.values();
}
}
son:
package com.lsp;
import java.util.Collection;
import java.util.Map;
public class Son extends Father {
public Collection toMap(Map map) {
System.out.println("子类被 执行....");
return map.values();
}
}
client:
package com.lsp;
import java.util.HashMap;
public class Client {
public static void main(String[] args) {
Father father = new Father();
father.toMap(new HashMap());
Son son = new Son();
son.toMap(new HashMap());
}
}
结果:
父类被执行....
父类被执行....
现在问题来了,为什么两次都是调用父类的方法呢?方法名虽然相同,但方法的输入参数不同,就不是重写而是重载。继承就是,子类拥有父类的所有属性和方法,方法名相同,输入参数不相同,当然是重载了。根据里氏规制原则,父类出现的地方子类就可以出现。要注意的是:子类中方法的前置条件必须与超类中被覆写方法的输入参数类型要宽或者相同
第四种含义:覆写或实现父类方法时,输出结果可以被 缩小
先看例子:
father:
package com.lsp;
import java.util.LinkedList;
import java.util.List;
public class Father {
public List toList() {
System.err.println("父类被执行。。。。");
return new LinkedList<>();
}
}
son:
package com.lsp;
import java.util.LinkedList;
public class Son extends Father {
public LinkedList toList() {
System.out.println("子类被执行。。。。");
return new LinkedList<>();
}
}
client:
package com.lsp;
public class Client {
public static void main(String[] args) {
Father father = new Father();
father.toList();
Son son = new Son();
son.toList();
}
}
结果:
父类被执行。。。。
子类被执行。。。。
父类的方法返回值是类型T,子类的相同方法(覆盖或重写)的返回类型K,那么里氏替换原则就要求K必须小于等于T,也就是说,要么K与T是同一个类型,要么K是T的子类,为什么呢?分两种情况:如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值K小于等于T,这就是覆写的要求。如果是重载,在里氏原则的要求下,则要求方法的输入参数类型或数量不相同,也就是子类的输入参数宽于或等于父类的输入参数,也就是你写的这个方法不会被调用。
采用里氏替换原则,可以增强程序健壮性,版本升级时,也可以保持好的兼容性。