里氏替换原则
定义:
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S就是类型T的子类型。
所有引用基类的地方必须能透明地使用其子类对象。
首先看继承的优缺点:
继承的优点
1.代码共享,提高代码的复用性,每个子类都拥有父类的属性和方法
2.子类虽然拥有父类的方法,但是也可以拥有自己的特性。即可扩展性
继承的缺点
1.继承有侵入性,只要继承,那么就会拥有父类的属性和方法
2.因为子类拥有父类的属性和方法,所以子类的约束也就变多了,也就降低了子类的灵活性
3.增强了耦合性,当父类的常量,变量,方法被修改时,子类也要随之改变
里氏替换原则就是定义了继承的规范:
1.子类必须完全实现父类的方法
通常我们在设计系统时,会先定义接口或者是抽象类,然后编码实现,调用类则直接传入接口或抽象类。
①设计一个接口或者是抽象类
public abstract class AbstractGun {
/**
* 射击
*/
public abstract void shoot();
}
②设计实现类
public class HandGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("手枪射击敌人");
}
}
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("机枪扫射敌人");
}
}
public class Rifle extends AbstractGun {
@Override
public void shoot() {
System.out.println("步枪射击敌人");
}
}
③传入接口或者抽象类
public class Soldier {
private AbstractGun abstractGun;
public void setAbstractGun(AbstractGun abstractGun) {
this.abstractGun = abstractGun;
}
public void killEnemy() {
//这里注意,在类中如果调用其他类时,务必要使用父类或者抽象类接口,否则就违背了里氏替换原则
abstractGun.shoot();
}
}
里氏替换原则第一条:
子类必须完全实现父类的方法
注意事项
1.在类中调用其他类时,一定传入的是接口或者抽象类,否则违背了里氏替换原则
2.如果子类不能完全实现父类的方法,或者父类的方法在子类中产生畸变,那么建议断开父子继承关系
子类可以有自己的个性
里氏替换原则第二条:
在父类能出现的地方子类一定能出现,但是子类能出现的地方,父类不一定能出现。
覆盖或实现父类的方法时输入参数可以被放大
如果是重载被放大,则导致只会执行父类方法,子类方法不会被执行
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) {
invoker();
}
public static void invoker(){
Father f = new Father();
HashMap map = new HashMap();
f.doSomething(map);
}
}
执行了父类的方法。
根据里氏替换原则,将父类出现的地方替换成子类。
测试:
public class Client {
public static void main(String[] args) {
invoker();
}
public static void invoker(){
//根据里氏替换原则,父类出现的地方,子类一定可以出现,所以我们修改
//Father f = new Father();
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
}
执行了父类的方法,子类方法永远不会被执行。
如果父类的前置条件大,子类的前置条件小
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) {
invoker();
}
public static void invoker(){
//根据里氏替换原则,父类出现的地方,子类一定可以出现,所以我们修改
//Father f = new Father();
Son f = new Son();
HashMap map = new HashMap();
f.doSomething(map);
}
}
测试结果,子类的方法被执行了。但是这不对啊,如果父类是一个抽象类,你在没有重写父类抽象方法的情况下,子类方法被执行了,这是不符合你设计的业务逻辑的。
总结:在重写或实现父类的方法时,子类的前置条件应该比父类的前置条件相同或者更宽松,但是不能比父类的前置条件小。
重写或实现父类的方法时,输出结果可以被缩小
意思就是,如果父类的方法返回值类型时T,子类重写或重载父类方法的返回值类型是S,那么要求S必须小于等于T。
总结:
继承给我们带来便捷,但是也带来了约束,在设计时,满足里氏替换原则必须规范以下4点:
- 子类必须完全实现父类的方法,在类中调用其他类时,必须使用接口或者抽象类,否则违反里氏替换原则
- 子类可以拥有自己的个性,里氏替换原则规定,在父类出现的地方一定能够出现子类,但是在子类能出现的地方,不一定能出现父类
- 重写或实现父类的方法时,子类的前置条件应该大于等于父类的前置条件
- 重写或实现父类的方法时,子类的返回值类型应该小于等于父类的返回值类型