里氏替换原则
定义:所有引用基类的地方必须能透明地使用其子类的对象,通俗点讲,将父类替换为子类后,程序不会产生错误或异常。
里氏替换原则的定义包含了四层含义
1.子类必须完全实现父类的方法
(可以通过父类来对子类进行限制,起到一定的规范子类的作用)
举个栗子,描述cs中的枪,图2-1:
那这个类图就很明显的表述了里氏原则,士兵获得枪,可以是手枪,步枪,机枪,代码如下:
public abstract class AbstractGun{
//枪的方法,射击
public abstract shoot();
}
//手枪
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("手枪射击");
}
}
//步枪
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.println("步枪射击");
}
}
//机枪
public class MechinaGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("机枪射击");
}
}
//士兵类
public class Soldier {
private AbstractGun gun;
public AbstractGun getGun() {
return gun;
}
public void setGun(AbstractGun gun) {
this.gun = gun;
}
public 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();
}
}
运行结果:
士兵开始设计
步枪射击
这里父类可以用子类替换,并且不同子类的射击方式不一样,这不就是java的继承和多态的特性;
现在继续想一下,如果有一把玩具枪,怎么定义,现在类图2-1上增加一个类ToyGun,继承AbstractGun类,修改后的类图如图2-2:
代码如下:
public class ToyGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("这是一把玩具枪,打不死敌人。。。");
}
}
public class Client {
public static void main(String[] args) {
Soldier sanmao = new Soldier();
sanmao.setGun(new ToyGun ());
sanmao.killEnemy();
}
}
发现玩具枪无法击杀敌人,那就玩完了,只能等着被爆头。。。
(感觉这个例子很牵强,哈哈)
这个业务场景就是玩具枪不满足条件,但他又确实继承abstractGun类,这样可以将toyGun与abstractGun断开继承关系,建立一个独立的父类AbstractToy,让两个基类下的子类自由发展,互不影响,如图2-3:
注意:如果子类不能完整的实现父类方法,或者父类的么些方法在子类中发生"畸变",则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承
(这个好像在图2-3里面看不出什么来)
2子类可以有自己的个性.
子类当然可以有自己的方法和属性,由于这个原因,子类能胜任的地方,父类不一定能适用,例如,步枪有几个比较响亮的型号,比如ak47,aug狙击步枪等,类图2-4:
//ak47
public class Ak47 extends Rifle {
}
//aug
public class Aug extends Rifle {
public boolean zoomOut(){
System.out.println("瞄准敌人");
return true;
}
}
//狙击手
public class Snipper {
private Aug aug;
public Aug getAug() {
return aug;
}
public void setAug(Aug aug) {
this.aug = aug;
}
public void killEnemy() {
if(aug.zoomOut()){
aug.shoot();
}
}
}
public class Client {
public static void main(String[] args) {
Snipper sanmao = new Snipper();
sanmao.setAug(new Aug());
sanmao.killEnemy();
}
}
(其实这个我写代码的时候想这个snipper是不是应该继承soldier,狙击手也是士兵的一种,然后让他拥有狙击枪的属性,然后重写killEnemy的具体实现。但是做完后好像很混乱,如果用父类的setGun方法会导致报错,因为abstractGun没有瞄准的方法)
而且向下转型是不安全的。
-
覆盖或实现父类的方法时输入参数可以被放大
方法中的输入参数称为前置条件,就是你要让我执行,就必须满足我的条件,可以理解为执行方法必须按我要求的参数数量个类型传入,后置条件就是执行完了返回的类型return,这种方式也叫契约设计。
//父类
public class Father {
public Collection doSomething(HashMap map){
System.out.println("父类执行了");
return map.values();
}
}
//子类
public class Son extends Father{
//注意看这是一个重载方法
public Collection doSomething(Map map) {
System.out.println("子类被执行了");
return map.values();
}
}
//测试
public class Client {
public static void main(String[] args) {
Father father = new Father();
father.doSomething(new HashMap());
Son son = new Son();
son.doSomething(new HashMap());
}
}
运行结果:
父类执行了
父类执行了
这就是我们要的效果,父类出现的地方,子类可以胜任,在没有覆写父类方法的情况下并不会改变父类的执行逻辑
现在改一下,让子类的方法前置条件范围小于父类方法的前置条件
//父类
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 main(String[] args) {
Father father = new Father();
father.doSomething(new HashMap());
Son son = new Son();
son.doSomething(new HashMap());
}
}
运行结果:
父类执行了
子类执行了
咋一看好像没啥毛病,但是注意这个子类执行的方法并不是覆写父类的方法,它是重载方法被执行了,那么这个地方就有可能改变了父类的意图,父类一般是抽象方法,但是这里并不是实现了这个抽象方法,而是重载了一个方法,这有可能造成逻辑混乱,所以子类中方法的前置条件一定要与超类中被覆写的方法的范围相同或更大,(这个描述有点不准确,参数的范围改了是重载而不是覆写)
-
覆写或实现父类的方法时输出结果可以被缩小
父类的一个方法返回值是类型T,那么子类覆写方法的返回值S,那么S必须小于等于T,也就是说,要么S和T是同一个类型要么S是T的子类。(原书中好像表述有点问题,返回值类型不同时,应该是只能是覆写,重载的定义是返回值相同,参数类型和数量可以不同)
里氏替换原则应该是很好的阐述了多态这一特性,基本就是父类做参数,传递子类去对应不同的业务逻辑,非常完美。
内容来自《设计模式之禅》