里氏替换原则

作者: 小周佩奇i | 来源:发表于2021-08-23 14:10 被阅读0次

    定义:在面向对象的程序设计中,里氏替换原则(Liskov Substitution principle)是对子类型的特别定义。里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”

    原理:在软件开发过程中,若子类重写了父类方法,当用子类代替父类时就会出现逻辑不一致的问题。

    问题由来:类A实现了方法a,而其子类A1覆写了该方法,则当子类出现在父类定义的类型时可能会出错(针对继承时),如下:

    public class Main {
    
        public static void main(String[] args) {
            A[] as = new A[]{
                    new A(), new A1()
            };
            for (A a : as) {
                a.a();
            }
        }
    
    }
    
    class A {
        public void a() {
            System.out.println("A#a");
        }
    }
    
    class A1 extends A {
        @Override
        public void a() {
            System.out.println("A1#a");
        }
    }
    
    output
    A#a
    A1#a
    

    可以看到,数组中的第二个对象是A1的实例,而A1覆写来了方法a,此时虽然定义的类型是A,到那时由于A1是A的子类,其是可以替换类型A的对象的,而由于方法a被覆写,输出内容超出了预想内容。

    产生原因:软件开发时类型把控不严格可能会导致此问题

    解决办法:

    1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    2. 子类可以增加自己特有的方法
    3. 当子类的方法重载父类的方法时,方法的形参要比父类方法的输入参数更宽松
    4. 当子类的方法实现父类的抽象方法时,方法的返回值应比父类更严格

    应用场景:

    针对继承时

    如果继承是为了实现代码重用时,那么共享的父类方法应该保持不变,不能被子类重新定义,为了避免出现此类问题,我们应该在共享的父类方法中加上final字段,防止子类不小心覆写了共享的父类方法。子类只能通过新添加方法来扩展功能,那么上面的代码应该修改为如下形式:

    public class Main {
    
        public static void main(String[] args) {
            A[] as = new A[]{
                    new A(), new A1()
            };
            for (A a : as) {
                a.a();
            }
        }
    
    }
    
    class A {
        public final void a() {
            System.out.println("A#a");
        }
    }
    
    class A1 extends A {
        public void a1() {
            System.out.println("A1#a1");
        }
    }
    
    
    A#a
    A#a
    

    可以看到,此时即便在A类的数组中实例化的时A1,我们调用方法a时的逻辑还是A的逻辑,而A1需要扩展功能我们则新添加一个方法a1,让其实现A1需要扩展的功能。

    针对多态时

    如果继承的目的时为了多态,而多态的前提是子类覆盖并重新定义父类方法,为了符合里氏替换原则,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类是,父类就不能实例化,所以也不存在可实例化的父类对象在程序里。也就规避了子类替换父类实例时逻辑不一致的问题。如下:

    public class Main {
    
        public static void main(String[] args) {
            A[] as = new A[]{new A1(), new A2()};
            for (A a : as) {
                a.a();
            }
        }
    
    }
    
    abstract class A {
        abstract void a();
    }
    
    class A1 extends A {
        @Override
        void a() {
            System.out.println("A1#a");
        }
    }
    
    class A2 extends A {
        @Override
        void a() {
            System.out.println("A2#a");
        }
    }
    
    A1#a
    A2#a
    

    此时A类的定义是为了多态,同时我们将A类设为了抽象类并将方法a设为抽象方法,因此A类不可以被实例化且所有子类都必须实现该方法,所以也就不存在父类逻辑,因此也就规避了子类与父类逻辑不一致的可能。注:若方法较多,且让子类选择实现时可以不设为抽象方法,单父类必须设为抽象类,且需要子类实现的方法必须为空方法。此时需要共享的方法要添加上final关键字,防止子类不小心实现了共享方法。若我们希望某类不能被继承时也需要将类设为final类。

    小提示:final 关键字很重要,在必要时不要忘了加上它。添加final关键字时不仅可以提高代码可读性同时也可以增加代码的性能。

    相关文章

      网友评论

        本文标题:里氏替换原则

        本文链接:https://www.haomeiwen.com/subject/kccliltx.html