里氏替换原则

        里氏替换原则主要是发生在父类和子类之间,说到父类和子类,在面向对象的语言中, 继承是必不可少的、 非常优秀的语言机制, 它有如下优点

  • 代码共享,减少创建类的代码量,每一个子类可以拥有父类的方法和属性。
  • 提高代码的重用性。
  • 子类可以形似父类,但又异于父类。
  • 提高代码的扩展性。

        缺点

  • 继承是侵入性的。 只要继承, 就必须拥有父类的所有属性和方法;
  • 降低代码的灵活性。 子类必须拥有父类的属性和方法, 让子类自由的世界中多了些约
    束;
  • 增强了耦合性。 当父类的常量、 变量和方法被修改时, 需要考虑子类的修改, 而且在
    缺乏规范的环境下, 这种修改可能带来非常糟糕的结果——大段的代码需要重复。

目录

定义

含义

1.子类必须完全实现父类的方法。

2.子类可以有自己的个性

3. 覆盖或实现父类的方法时输入参数可以被放大。

4.覆写或实现父类的方法时输出结果可以被缩小

最佳实践


定义

        里氏替换法则有两种定义:

  • 如果对每一个类型为 T1 的对象 o1,都有类型为 T2 的对象 o2,使得以 T1 定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。
  • 所有引用基类(父类)的地方必须能透明地使用其子类的对象。

        第二个定义是最清晰明确的,通俗点讲只要父类能出现的地方我子类就可以出现,而且调用子类还不产生任何的错误或异常,调用者可能根本就不需要知道是父类还是子类。但是反过来就不成了,有子类出现的地方,父类未必就能适应。

含义

        里氏替换原则有4层含义。

  • 子类必须完全实现父类的方法。
  • 子类可以有自己的个性。
  • 覆盖或实现父类的方法时输入参数可以被放大。
  • 覆写或实现父类的方法时输出结果可以被缩小。        

1.子类必须完全实现父类的方法。

        子类必须实现父类的抽象方法,但不得重写(覆盖)父类的非抽象(已实现)方法.

//父类
public class Father{
    public void add(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
}
//子类
public class Son extends Father{
    @Override
    public void add(int a,int b){
        System.out.println(a+"-"+b+"="+(a-b));
    }
}


 public static void main(String[] args){
        System.out.println("父类的运行结果");
        Father father=new Father();
        father.add(1,2);
        //父类存在的地方,可以用子类替代
        //子类B替代父类A
        System.out.println("子类替代父类后的运行结果");
        Son  son=new Son ();
        son.add(1,2);
    }
运行结果:
父类的运行结果
1+2=3
子类替代父类后的运行结果
1-2=-1 

        我们定义的这个add方法是加数,而不是减数,子类破坏了父类的加数方法。

2.子类可以有自己的个性

        子类中可以增加自己特有的方法。

//父类
public class Father{
    public void add(int a,int b){
        System.out.println("父类:"+a+"+"+b+"="+(a+b));
    }
}
//子类
public class Son extends Father{
    public void reduce(int a,int b){
        System.out.println("子类:"+a+"-"+b+"="+(a-b));
    }
}


 public static void main(String[] args){
        Father father=new Father();
        father.add(1,2);
        
        Son son=new Son ();
        son.add(2,3);
        son.reduce(2,1);
    }

运行结果:

父类:1+2=3

父类:2+3=5

子类:2-1=1

3. 覆盖或实现父类的方法时输入参数可以被放大。

        当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

public class Father {
    public Collection doSomething(HashMap map){
        System.out.println("父类被执行...");
    return map.values();}
}


public class Son extends Father {
    //放大输入参数类型,与父类的方法名相同, 但又不是覆写(Override) 父类的方法。 你加
    //个@Override试试看, 会报错的, 为什么呢? 方法名虽然相同, 但方法的输入参数不同, 就
    //不是覆写, 
    public Collection doSomething(Map map){
        System.out.println("子类被执行...");
        return map.values();
    }
}

        我们来调用一下Father 和Son 类的doSomething方法


public static void main(String[] args) {
 //父类存在的地方, 子类就应该能够存在
    Father f = new Father();
    Son son = new Son();
    HashMap map = new HashMap();
    f.doSomething(map);
    son.doSomething(map);
}

运行结果:

父类被执行了

父类被执行了

        运行结果两个是一样的, 看明白是怎么回事了吗? 父类方法的输入参数是HashMap类型, 子类的输入参数是Map类型, 也就是说子类的前置条件(输入参数类型)的范围扩大了, 子类代替父类传递到调用者中, 子类的方法永远都不会被执行。 这是正确的。

        如果我们把这个前置条件反过来,看下边代码。

public class Father {
    public Collection doSomething(Map map){
        System.out.println("父类被执行...");
    return map.values();}
}


public class Son extends Father {
    //放大输入参数类型,与父类的方法名相同, 但又不是覆写(Override) 父类的方法。 你加
    //个@Override试试看, 会报错的, 为什么呢? 方法名虽然相同, 但方法的输入参数不同, 就
    //不是覆写, 
    public Collection doSomething(HashMap map){
        System.out.println("子类被执行...");
        return map.values();
    }
}



public static void main(String[] args) {
 //父类存在的地方, 子类就应该能够存在
    Father f = new Father();
    Son son = new Son();
    HashMap map = new HashMap();
    f.doSomething(map);
    son.doSomething(map);
}

运行结果:

父类被执行了

子类被执行了

        运行结果不一样的, 看明白是怎么回事了吗? 父类方法的输入参数是Map类型, 子类的输入参数是HashMap类型, 也就是说子类的前置条件(输入参数类型)的范围缩小了, 子类代替父类传递到调用者中, 父类的方法永远都不会被执行。 这是错误的,(主要第一条不得重写(覆盖)父类的非抽象(已实现)方法),明白了没啊。

4.覆写或实现父类的方法时输出结果可以被缩小

        当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

        

abstract class Father {
    public abstract Map getMap();
}         
class Son extends Father{
     @Override
     public HashMap getMap(){
         HashMap b=new HashMap();
         b.put("b","子类被执行...");
         return b;
     }
}
         
public static void main(String[] args){
      Father father=new Father();
      System.out.println(father.getMap());
}

        注意:HashMap是Map的子类,        

        父类的一个方法的返回值是一个类型T(Map), 子类的相同方法(覆写) 的返回值为S(HashMap), 那么里氏替换原则就要求S(HashMap)必须小于等于T(Map), 上边的案例完全满足这个条件。

        如果我们要是相反,有什么结果?

        程序直接就报错了,什么报错信息了?

报错信息

英文:'getMap()' in 'com.fry.mylibrary.oaid.MiitHelper.Son' clashes with 'getMap()' in 'com.fry.mylibrary.oaid.MiitHelper.Father'; attempting to use incompatible return type

中文:'com.fry.mylibrary.oaid.MiitHelper.Son' 中的 'getMap()' 与 'com.fry.mylibrary.oaid.MiitHelper.Father' 中的 'getMap()' 冲突; 尝试使用不兼容的返回类型 

        在里氏替换原则 要么S和T是同一个类型, 要么S是T的子类, 为什么呢? 分两种情况, 如果是覆写, 父类和子类的同名方法的输入参数是相同的, 两个方法的范围值S小于等于T, 这是覆写的要求, 这才是重中之重, 子类覆写父类的方法, 天经地义。 如果是重载, 则要求方法的输入参数类型或数量不相同, 在里氏替换原则要求下, 就是子类的输入参数宽于或等于父类的输入参数, 也就是说你写的这个方法是不会被调用的, 参考上面讲的前置条件。

最佳实践

        在项目中, 采用里氏替换原则时, 尽量避免子类的“个性”, 一旦子类有“个性”, 这个子
类和父类之间的关系就很难调和了, 把子类当做父类使用, 子类的“个性”被抹杀——委屈了
点; 把子类单独作为一个业务来使用, 则会让代码间的耦合关系变得扑朔迷离——缺乏类替
换的标准。

参考书籍:设计模式之禅。
 

        

        

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值