概述
目录
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大原则之(二):里氏替换原则所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复