我是靠谱客的博主 感动冷风,最近开发中收集的这篇文章主要介绍Java 设计模式6大原则之(二):里氏替换原则,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

 目录

        Java 设计模式6大原则之(一):开闭原则

        Java 设计模式6大原则之(二):里氏替换原则

        Java 设计模式6大原则之(三):依赖倒置原则

        Java 设计模式6大原则之(四):接口隔离原则

        Java 设计模式6大原则之(五):合成/聚合复用原则

        Java 设计模式6大原则之(六):迪米特法则


里氏替换原则(LSP)

  • 定义

        如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T2定义的所有程序P在所有的对象o1都换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。

  • 简单理解

        任何基类可以出现的地方,子类一定可以出现。

  • 详细描述

        在代码中将一个基类对象替换成它的子类对象,程序不会产生任何错误和异常,反过来则不成立,如果一个类实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

例如:我喜欢动物,那我一定喜欢狗,因为狗是动物的子类;但是我喜欢狗,不能据此断定我喜欢动物,因为我并不喜欢老鼠,虽然它也是动物。

  • 场景分析        

        例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代替不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base)。

class BaseClass{
    ...
}
class SubClass extends BaseClass{
    ...
}
class OtherClass{
    //此处的BaseClass能够替换成子类的SubClass类型对象
    public void method1(BaseClass base){
            
    }
    //反过来,此处的SubClass对象不能够替换为BaseClass对象    
    public void method2(SubClass sub){
        
    }
}

我们在做系统设计时,经常会定义一个接口或者抽象类,然后编码实现,调用类则直接传入接口或抽象类, 其实这已经使用了里氏替换原则。

我们举一个拿手机打电话的例子,结构组成如下 

public class Client {
 
    public static void main(String[] args) {
        Person person = new Person();
 
        //设置用户拿固定电话
        person.setPhone(new FixedPhone());
        person.startCall();
 
        //设置用户拿移动电话
        person.setPhone(new MobilePhone());
        person.startCall();
    }
}
 
abstract class AbstractPhone {
    //抽象打电话的方法
    public abstract void call();
}
 
class FixedPhone extends AbstractPhone {
 
    @Override
    public void call() {
        System.out.println("用固定电话打电话");
    }
 
}
 
class MobilePhone extends AbstractPhone {
 
    @Override
    public void call() {
        System.out.println("用移动电话打电话");
    }
}
 
//用户打电话
class Person {
    private AbstractPhone phone;
 
    public void setPhone(AbstractPhone phone) {
        this.phone = phone;
    }
 
    public void startCall() {
        System.out.println("用户开始打电话");
        this.phone.call();
    }
}

        里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象

        里氏替换原则的另外一层含义是子类可以扩展父类的功能,但不能改变父类原有的功能,主要作用就是规范集成时子类的一些书写规则,接下来我们看看继承规范时的书写规则有哪些。

  • 里氏替换原则继承规范

        里氏替换原则对继承进行了规则上的约束,这种约束主要体现在四个方面:                      

       ○子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法(视情况而定)

       以下例举出Java小白开发者的编码,看看覆盖了父类的抽象方法后会带来什么后果

//类A完成,两个数相加的功能
class A {
    public int fun1(int a,int b){
        return a + b;
    }
}

//Demo执行
public class Demo3 {
    public static void main(String[] args){
        A a = new A();
        System.out.println("5+2的结果为:" + a.fun1(5,2));    
    }
}  

运行结果是:5+2的结果为:7   

        随着业务的发展,我们需要新增一个功能,完成2个数相减,并对其结果再减去10,考虑到扩展性,我们不应该改动原来的代码,所以我们重新定义一个类B完成,如下。

public class Demo3 {

    public static void main(String[] args) {
        B b = new B();
        System.out.println("25+2的结果为:" + b.fun1(25, 2));
        System.out.println("18+2的结果为:" + b.fun1(18, 2));
        System.out.println("18-2-10的结果为:" + b.fun2(18, 2));
        //程序输出:25+2的结果为:23
        //程序输出:18+2的结果为:16
        //程序输出:18-2-10的结果为:6
    }
}

//类A完成,两个数相加的功能
class A {
    public int fun1(int a, int b) {
        return a + b;
    }
}

//类B,在类A的基础上扩展
class B extends A {
    public int fun1(int a, int b) {
        return a - b;
    }

    public int fun2(int a, int b) {
        return fun1(a, b) - 10;
    }
}

        此时你会发现,我们原本以为在类A的基础上去扩展的类B会正确输出,但实际上类B已经覆盖了类A中已实现的方法,所以出现了错误的结果。虽然这段代码我们能够看出来有问题,但实际开发中,在不知情的情况下覆盖了父类的非抽象方法,这会带来意想不到的错误。

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

        这个规划我们通过继承就可以知道,子类可以扩展自己的行为和属性,那为什么要在里式替换中提出呢?主要是因为里氏替换原则是说子类可以胜任父类的任何工作,但父类不一定能够替换子类,所以提出该规则

        我们在上述打电话的案例中新增一个例子,比如移动电话还有其子类产品,比如OPPO、HuaWei、Mi

public class Client {

    public static void main(String[] args) {
        //子类可以直接调用
        User user = new User();
        user.startCall(new HuaWei());

        //如果现在指定了子类,你要传父类,会出现编译错误,即使向下强转也会出现运行异常
        User user1 = new User();
        user1.startCall((HuaWei)(new MobilePhone()));//出现异常
    }
}

class HuaWei extends MobilePhone{
    public void unlock(){
        System.out.println("打电话之前解锁");
    }

    public void call(){
        System.out.println("华为手机打电话");
    }
}

//动作处理,由于扩展了属性,所以不再适合在之前的Person类中修改,新建一个User类直接处理
class User{
    public void startCall(HuaWei huaWei){
        huaWei.unlock();
        huaWei.call();
    }
}

        ○当子类覆盖或实现父类的方法时,方法的输入参数(形参)要比父类方法的输入参数更宽松,不能相同。有如下代码,运行结果为:父类被执行。

public class Client {

    public static void main(String[] args) {
        Father father = new Father();
        HashMap<String,String> map = new HashMap<>();
        father.doSomething(map);
    }

}

//父类
class Father {

    public Collection<String> doSomething(HashMap<String, String> map) {
        System.out.println("父类被执行");
        return map.values();
    }

}

//子类
class Son extends Father {

    public Collection<String> doSomething(Map<String, String> map) {
        System.out.println("子类被执行");
        return map.values();
    }

}

运行结果是:父类被执行

根据里氏替换原则,父类出现的地方,子类也是可以出现的。我们把Client代码修改如下:

public class Client {

    public static void main(String[] args) {
        //父类存在的地方,子类应该可以存在,而且结果应与父类相同
//        Father father = new Father();
        Son father = new Son();
        HashMap<String, String> map = new HashMap<>();
        father.doSomething(map);
    }

}

运行结果依然是:父类被执行

结果一样,父类的方法的输入参数是HashMap类型,子类的方法的输出的参数是Map类型,也就是说子类的输入参数类型范围扩大了,子类代替父类,子类的方法不被执行,这是正确的,如果你想让子类的方法运行,就必须覆写父类的方法。

如果,我们反过来,把父类的输入参数类型放大、子类的输入参数类型缩小,让子类的输入参数类型小于父类的参数类型,看看会出现什么情况?

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

public class Client {

    public static void main(String[] args) {
        System.out.println("父类的运行结果");
        C c = new C();
        HashMap<String, String> map = new HashMap<>();
        c.fun(map);

        //父类存在的地方,都可以用子类替代
        //子类B替代父类A
        System.out.println("子类替代父类后的运行结果:");
        D d = new D();
        d.fun(map);
    }

}

//错误示例
//父类
class C {
    public void fun(Map<String, String> map) {
        System.out.println("父类被执行...");
    }
}

//子类
class D extends C {
    public void fun(HashMap<String, String> map) {
        System.out.println("子类被执行...");
    }
}

运行结果是:父类被执行...

子类替代父类后的运行结果是:子类被执行...

        ○当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格(范围更小)。

public class Client {

    public static void main(String[] args) {
        E f = new F();
        System.out.println(f.fun());
    }

}

abstract class E {
    public abstract Map<String, String> fun();
}

class F extends E {

    @Override
    public HashMap<String, String> fun() {
        HashMap<String, String> f = new HashMap<>();
        f.put("f", "子类被执行...");
        return f;
    }
    
}

若在继承时,子类的方法返回值类型范围比父类的方法返回值类型范围大,在子类重写该方法时编译器会报错。

  • 以上就是里氏替换原则知识点,有一定的参考作用,但无需严格遵守,在实际开发中根据实际情况来遵守

最后

以上就是感动冷风为你收集整理的Java 设计模式6大原则之(二):里氏替换原则的全部内容,希望文章能够帮你解决Java 设计模式6大原则之(二):里氏替换原则所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(56)

评论列表共有 0 条评论

立即
投稿
返回
顶部