Java学习笔记-设计模式:里氏替换原则

里斯替换原则定义:

  1. 定义一: 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得在使用了T1类型的程序P中,将所有的T1类型对象o1替换成T2类型对象o2后,程序P的行为(功能)没有发生任何变化,那么类型T2就是类型T1的子类型.
  2. 定义二: 所有引用基类的地方,必须能够透明的引用其子类对象.

理解:

  • 子类就是一个父类,父类能够出现的地方子类一定能够出现。
  • 父类能够完成的功能子类也能够完成.

里斯替换原则内在含义:

  1. 含义一:子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法.【核心观念】
  2. 含义二:子类中可以增加自己特有的方法.
  3. 含义三:前置条件放大, 子类在重载父类的已实现方法时,方法的前置条件(形参)范围应该比父类更加宽松.
  4. 含义四:后置条件缩小,子类在实现父类的抽象方法时,方法的后置条件(返回值)范围应该比父类更加严格或相同.

理解一:针对1,子类不要修改或覆盖父类已经实现好的方法。

问题:为什么子类不要修改或覆盖父类已经实现好的方法?

  • 违反里氏替换原则定义

违反了里氏替换原则对"子类就是一个父类,父类能够出现的地方子类一定能够出现"的定义,当子类和父类的已实现方法功能不同时,子类就不一定能够完全的替换掉父类(功能不一致)。

  • 达不到抽取继承体系中重复代码的目的:

我们使用继承的很大部分原因是为了抽取可重用的重复代码,减少同一“类型”中代码的重复定义。如果在一个继承体系中大部分子类都需要覆盖重写父类的方法,那么便达不到减少重复代码的作用。

  • 造成方法调用的混乱:

父类中已经实现好的方法,相当于是在定义一系列的具体的行为规则(与接口不同,接口是一套行为锲约的抽象定义,并且实现类必须实现),虽然父类不要求所有的子类必须遵守这一规则(子类可以修改),但是如果子类随意修改父类的这一"规则",则造会成整个继承体系的混乱。

如果父类中已经实现好的方法被若干个子类随意地进行覆盖重写,那么在该继承体系中就会产生同一方法对应着若干个不同的实现。即,当使用不同子类的多态类型时就会面临着同一方法声明却又不同实现(功能)的场景,这样就会造成整个继承体系地混乱和模糊,尤其是在频繁使用到该继承体系的多态时,就会增大出错的可能性. 比如:

// 父类
public class Parent {

    // 父类中已经实现好的方法: 求和功能
    public int function(int a, int b) {
        return (a + b);
    }

}


// 子类1
public class Son1 extends Parent {

    // 覆盖重写父类中的getSum()方法: 相减功能
    @Override
    public int function(int a, int b) {
        return (a - b);
    }

}

// 子类2
public class Son2 extends Parent {

    // 覆盖重写父类中的getSum()方法: 相除功能
    @Override
    public int function(int a, int b) {
        return (a / b);
    }

}


// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        // 2: 求和
        int num = son1.function(1, 2);
    
    }

}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son1 son1 = new Son1();
        
        // 2: 原程序需要的是求和功能,此时同一方法却变成了相减功能。
        int num = son1.function(1, 2);
    
    }

}

综上所述,如果一个继承体系中父类中的部分方法需要频繁的被子类实现,那么还不如在各个子类中分别定义相应的方法。因为,它们不属于重复代码,同时在相应的子类中定义也可以为不同功能的代码定义“见名知意”方法签名。

理解二:针对2,子类可以有自己的“特色”

子类可以在父类的基础之上,增加自己特有的功能。这样子类可以完成更多的功能,并且还可以完全透明的替换掉父类。

理解三:针对3,方法重载的规则

1、什么是方法的前置条件和后置条件?

方法,即对具有某一行为或功能的代码封装。前置条件,即方法的形参,是为方法功能的实现传递必须的数据;后置条件,即方法的返回值,得到功能方法的最终结果。

2、为什么将方法的形参和返回值称为“条件”?

方法的参数和返回值就是在定义该方法的"实现规则",即实现方法的功能所必须的前提条件以及最终得到什么类型的结果。具体如何实现不管,只要满足方法的前提和结果约束就行。通俗的将就是"完成某一件事,你必须先具有什么,最后必须得到什么"。

3、什么是方法的重载和重写?

(1)、子类覆盖重写父类方法规则:方法名相同、参数相同、异常缩小、权限放大、返回值相同或缩小。不满足不算覆盖重写 【方法条件相同 ,具体的实现方式不同】。使用重写后的方法替换掉原有的方法,原方法被替换掉。

              异常缩小:子类方法抛出的异常必须与父类声明的异常相同或是其子类异常。如果子类方法声明抛出的异常大于父类,那么子类就可能无法完全透明的替换掉父类。比如父类声明抛出的是一个空指针异常,而子类实现却抛出了个Exception异常,那么在使用子类替换父类时,原有程序的异常捕获机制可能就无法控制子类方法抛出的异常。

// 父类
public class Parent {

    // 父类中的方法,抛出空指针异常
    public int function(int a, int b) throws NullPointerException {
        return (a + b);
    }

}


// 子类
public class Son extends Parent {

    // 重载父类中的方法,异常范围放大:抛出Exception异常
    @Override
    public int function(int a, int b) throws Exception {
        return (a - b);
    }

}




// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        try {
            // 原程序的异常捕获机制只能捕获空指针异常
            int sum = parent.function(1, 2);
	} catch (NullPointerException e) {
			e.printStackTrace();
	}
    }
}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son son = new Son();
        
       
        try {
            // 使用子类替换掉父类后,原有程序的异常捕获机制就无法捕获子类抛出的异常
            int sum = son.function(1, 2);
	} catch (NullPointerException e) {
			e.printStackTrace();
	}
    
    }

}

              返回值相同或缩小:子类实现的方法返回值类型必须与父类相同或缩小。父如果子类实现的返回值大于父类,那么子类可能就无法完全透明的替换掉父类。比如父类声明返回值为List,子类却返回一个Collection,那么原程序可能就无法使用子类的返回值。此时子类便有可能不能完全透明的替换掉父类,即子类替换掉父类后可能会影响到原程序的运行状况,并且打破了原有方法的后置条件约束

// 父类
public class Parent {

    // 父类中的方法,返回List集合
    public List function() {
        return new ArrayList();
    }

}


// 子类
public class Son extends Parent {

    // 重载父类中的方法,后置条件放大:返回Set集合
    @Override
    public Collection function(int a, int b) {
        return new LinkedHashSet<>();
    }

}


// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        // 2: 获取List集合
        List list = parent.function();
    
    }

}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son son = new Son();
        
        // 2: 原程序需要的是List集合,此时同一方法却返回了Set集合。
        List list = son.function();
    
    }

}

   参数相同:父类的方法定义就是在定义一套"规则契约",实现该功能或行为必须满足。

 

(2)、方法重载的规则:方法名相同、参数列表不同 ---> 根据参数列表判断执行那个方法。现有方法的参数范围比较单一,需要增条该方法的条件接受范围,被重载的原方法和新方法都会存在,都可以调用。 【不同条件,不同实现】

 

4、重载和重写的意义?

  • 重载:已有方法的实现方式已经不适合了,需要根据原有条件重新实现,原有方法不再使用【针对该子类说】。
  • 重写:原有方法的参数条件不够"广泛",需要匹配更多的条件,但是原有方法还会使用。

5、重载和重写造成的结果?

  • 重写:原有方法被新的方法覆盖掉【仅仅针对该子类】。
  • 重载:原有方法和新的方法共存,根据参数列表来判断执行那个方法。

6、为什么子类在重载父类方法时需要前置条件放大?【重头戏】

子类方法与父类的方法参数可能存在以下三种情况:

  • 参数相同:属于重写,子类方法覆盖掉父类方法,只能执行子类中的方法,不符合方法重载的规定。
  • 参数范围缩小:属于重载,但是由于子类参数范围小于父类参数范围,那么便可能产生如下一种可能:
// 父类
public class Parent {

    // 父类中的方法,就收Collection集合及其子类
    public String function(Collection coll) {
        return "父类";
    }

}


// 子类
public class Son extends Parent {

    // 重载父类中的方法,前置条件缩小:接受Set集合及其子类
    @Override
    public String function(List list) {
        return "子类";
    }

}


// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        // 2: 传入List集合
        List list = new ArrayList<>();
        String str = parent.function(list); // 执行父类方法
        ......
        // 3: 再次传入Set集合
        Set set = new LinkedHashSet<>();
        String str = parent.function(set); // 执行父类方法
    
    }

}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son son = new Son(); 

        // 2: 传入List集合,
        List list = new ArrayList<>();
        String str = son.function(list); // 执行子类方法
        ......
        // 3: 再次传入Set集合
        Set set = new LinkedHashSet<>();
        String str = son.function(set); // 执行父类方法
    
    }

}

在传入参数时,若参数范围在子类方法的范围之内,那么执行的便是子类方法;若参数范围大于子类方法范围并且小于父类方法参数范围,那么此时执行的就是父类中的方法。

因此在使用子类替换掉父类后,使用同一对象调用同一方法却产生了不同的结果。此时子类便有可能不能完全透明的替换掉父类,即子类替换掉父类后可能会影响到原程序的运行状况。

  • 放大:重载,在使用父类的原程序中,参数的范围不可能大于父类参数所接受的最大范围,因此使用子类替换掉父类后执行的仍然是父类方法,子类可以完全透明的替换掉父类,并不会影响原程序的运行状态。
// 父类
public class Parent {

    // 父类中的方法,接受Collection集合及其子类
    public String function(Collection coll) {
        return "父类";
    }

}


// 子类
public class Son extends Parent {

    // 重载父类中的方法,前置条件放大:接受Object及其子类
    @Override
    public String function(Object obj) {
        return "子类";
    }

}


// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        // 2: 传入List集合
        List list = new ArrayList<>();
        String str = parent.function(list); // 执行父类方法
        ......
        // 3: 再次传入Set集合
        Set set = new LinkedHashSet<>();
        String str = parent.function(set); // 执行父类方法
    
    }

}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son son = new Son(); 

        // 2: 传入List集合,
        List list = new ArrayList<>();
        String str = son.function(list); // 执行父类方法
        ......
        // 3: 再次传入Set集合
        Set set = new LinkedHashSet<>();
        String str = son.function(set); // 执行父类方法
    
    }

}

 

理解三:针对4,方法重写的规则

在子类重写父类方法时,若子类方法的返回值大于父类,子类便可能无法完全的替换掉父类,替换掉父类后可能会影响到原程序的运行。

// 父类
public class Parent {

    // 父类中的方法,返回List集合
    public List function() {
        return new ArrayList();
    }

}


// 子类
public class Son extends Parent {

    // 重载父类中的方法,后置条件放大:返回Set集合
    @Override
    public Collection function(int a, int b) {
        return new LinkedHashSet<>();
    }

}


// 测试类:使用父类的原有程序
public class Test1 {

    public static void main(String[] args) {

        // 1: 使用父类
        Parent parent = new Parent(); 

        // 2: 获取List集合
        List list = parent.function();
    
    }

}


// 测试类:使用子类替换掉父类的新程序
public class Test2 {

    public static void main(String[] args) {

        // 1: 使用子类替换掉父类
        Son son = new Son();
        
        // 2: 原程序需要的是List集合,此时同一方法却返回了Set集合。
        List list = son.function();
    
    }

}

里氏替换原则本质上就是一句话"子类在替换掉父类后必须不能影响到原程序的运行状态"。

在实际开发中,为了达到可扩展的功能,我们经常会使用多态。而如果在子类的功能扩展中不遵守里氏替换原则,当我们使用扩展后的子类替换掉原有的父类时,可能就会影响到原程序的运行状态。

你可能感兴趣的:(设计模式)