《设计模式》——里氏替换原则

先扯两句

原本是不想扯了的,因为很久没扯了也不知道该说写什么,可是这里氏替换原则东西实在是太多了,我看过都快一周了,但是每次想写博客的时候,都写几个字就扔下了,倒不是说书中的内容不够详细,只是如果都是摘抄书的话,这个系列的意义也就没有了,而且从个人的角度来说,不能用自己的话说出来的东西,都不是自己的。

还好的是,总算是东拼西凑的时间完成了这篇博客,不至于像上一个系列一样无疾而终。坚持是一种好习惯,希望我能保持下去,与大家共勉吧。

下面才是这次扯的目的,哈哈哈,《设计模式》——目录,好了,闲言少叙,我们进入正题。

一. 继承

要说“里氏替换原则”,就要先知道什么是“里氏替换原则”,其实就是一个姓“里”的研究出来用于类替换的原则。
image

好吧,别打人,我不开玩笑了。。。

“里氏替换原则”实际是为良好的继承定义了一个规范,对于其定义,在书中共介绍了两种,这里我们自然是选择最浅显易懂的来说:

只要父类能出现的地方子类都可以出现,而且替换为子类也不会产生任何错误和异常,使用者根本不需要知道自己使用的事父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必能适应。

若要举个例子其实也简单:

private void getObject(Object o){

}

private void getString(String s){

}

image

很显然当我们要Object(父类)的时候,传递了个String(子类)也是可以通过的,但反过来,我们要String的时候,传Object却报错了。为什么呢?这就要先说说面向对象的三大特性之一——继承。

继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码

以上是继承的浅显定义,至于继承再详细的我这里给个链接吧,有兴趣的可以看看,当然,大家也可以自己查,网上还是有大把的讲解的

Java 继承

书中对于java的优点做了阐述,我这里也列举一下吧,或许从这些优点中,大家也能理解到究竟什么是继承,当然,也可能看了后更蒙(可以跳过,我就是这么干的)

  • 代码共享:减少创建类的工作量,每个子类都拥有父类的方法和属性(爸妈,给我打钱)
  • 提高代码的重用性(儿子,你张婶家孩子结婚,我没时间,你去一下)
  • 子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠的孩子会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指子与父的不同(你眼睛长得像你爸,鼻子像你妈)
  • 提高代码可扩展行,实现父类的方法就可以“为所欲为”了,君不见很多开源框架扩展接口都是通过继承父类来完成的(爸妈给我十块钱,我要买手套去搬砖赚钱)
  • 提高产品或项目的开放性(没事认个干儿子也是不错的选择)

当然,凡事都是两面的,不可能有什么只有优点没有缺点事物
image

继承的缺点:

  • 继承是入侵性的。只要继承,就必须拥有父类的所有属性和方法;(你老子的钱是你的,你老子的债也是你的)
  • 降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界多了些约束(看看现在那么多吐槽原生家庭的就应该明白了)
  • 增强了耦合性。当父类的常量、变量和方法被修改是,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大量代码的重构(父类瞒着儿子出去借高利贷)

二. 里氏替换原则

刚刚被继承一阵插科打诨,估计里氏替换原则是什么都快记不清了(反正我是忘了),那就重新看一下定义吧

只要父类能出现的地方子类都可以出现,而且替换为子类也不会产生任何错误和异常,使用者根本不需要知道自己使用的事父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必能适应。

不难看出,里氏替换原则就是为继承定义了一个优化使用的规范,“一句简单的定义包含了四层含义”(当然,这么有文化的话肯定不是我说的)

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

不用怀疑,即便是私有方法无法调用,但也是会在类内部执行的,谁让父类里就是这么做的。当然这里之所以需要强调,是为了证明为什么里氏替换原则父类出现的地方,子类都可以出现

public abstract class YuQianParent {
    abstract void smoking();
    abstract void drink();
}

public class YuQian extends YuQianParent {

    @Override
    void smoking() {
        
    }
    
     @Override
    void drink() {
        
    }
}

如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸形”,则建议断开父子继承关系,采用“依赖”、“聚集”、“组合”等关系代替继承(依赖、聚集、组合定义见附录——Java的依赖、关联、聚合和组合

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

public class YuQian extends YuQianParent{

    @Override
    void smoking() {
        
    }
    
    @Override
    void drink() {
        
    }
    
    void hotHead(){
        
    }
}

如上,于谦在实现了父类的抽烟、喝酒以外,子类还开发出了自己的特有技能——烫头。当然,肯定会有人说,这不是废话吗!毕竟我们在开发中就是这么用的。但之所以强调一下,是为了说明为什么里氏替换原则规定,子类出现的地方父类却不一定适用,无奈啊,谁让老一辈人没法烫头呢!

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

1)你是爹,我是爹?

这个部分书中举了一个例子:

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

public class Son extend Father {
    public Collection doSomething(Map map) {
        System.out.println("子类被执行...");
        return map.value;
    }
}

由于传入的参数一个是HashMap,一个是Map,并不是同一个参数,所以这里不是使用的重写(@Override),而是重载。

@Test
public void test() {
    Father f = new Father();
    HashMap map = new HashMap();
    f.doSomething(map);
}

其执行结果打印日志为

父类被执行了

而当将父类替换为子类的时候重新运行

@Test
public void test() {
    Son s = new Son();
    HashMap map = new HashMap();
    s.doSomething(map);
}

其执行结果打印日志仍然为

父类被执行了

原因,书中自然也给出了:

子类的输入参数类型的范围放大了(从HashMap变成他老子Map了),子类代替父类传递到调用者中,子类的方法永远都不会被执行。

换种说法就是,小学生买电脑,非要买1W的,结果拿他爸卡一刷,存款才500,那店员肯定是不能卖的。

在这里插入图片描述

如果非要执行怎么办,小学生等到大学毕业学会了赚钱的技能,自然可以自己赚钱买,而子类的方法多出了其他技能,如下,多了个iCanDoIt,明确指向子类的方法就可以了。当然,这段是实际应用的时候用来变通的,与本篇主旨无关。

public class Son extend Father{
    public Collection doSomething(Map map, Object iCanDoIt){
        System.out.println("子类被执行...");
        return map.value;
    }
}

上面的文字,虽然我打出来了,但是这个方法是提供给维护人员实在没有其他好的方法时采用的,之所以标为斜体,实在是这招不正。

不过前面说的是子类的参数设置为父类,父类的参数设置为子类(不说别的,看这句话就够乱的,这不禁让我想起以前给我父亲提意见的时候,他总能理直气壮地问我:“你是爹,我是爹?”,虽然我认为自己的想法更成熟,但是刚刚代码执行的结果大家也看到了,输出的还是父亲的想法啊)

在这里插入图片描述

2)你爹永远是你爹

但是如果我们按照正常的逻辑去考虑呢,儿子就是儿子,老子就是老子(子类的参数设置为子类,父类的参数设置为父类)又会怎么样呢?

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

public class Son extend Father {
    public Collection doSomething(HashMap map) {
        System.out.println("子类被执行...");
        return map.value;
    }
}
@Test
public void test() {
    Father f = new Father();
    HashMap map = new HashMap();
    f.doSomething(map);
}

其执行结果打印日志为

父类被执行了

而当将父类替换为子类的时候重新运行

@Test
public void test() {
    Son s = new Son();
    HashMap map = new HashMap();
    s.doSomething(map);
}

其执行结果打印日志仍然为

子类被执行了

辈分关系理清了,爹和儿子也都可以想干啥就干啥了,这下总可以爽了吧!!!

很遗憾,大家正各忙各的呢,突然厨房传来一声“碗”打碎的巨响,这可是祖传N多代的,家里唯一的一个饭碗啊,一家人可都指着它吃饭呢!这时候母亲出来了,看着一地的残骸和面面相觑的父子俩:“究竟是谁干的!”

儿子高喊:“我爸能打碎!”

当爸的一听不乐意了:“臭小子,你也能!”

当妈的怎么办,家里又没安监控,人生又不能回放,这打破碗的案子就成了悬案。过两天,衣服被蹭上油渍成了悬案、偷吃旅行前准备的蛋糕成了悬案……直到家被烧了都是一个个悬案。

当然,现实中不会这么惨,毕竟一句“你是爹,我是爹?”我就秒怂了,可类比到项目中呢,这个方法出了问题,是父类执行错了,还是子类执行错了?

所以同一个方法,子类、父类都可以执行的时候,维护人员就很难做到精准定位问题出在了哪里,增加了项目的维护难度。

子类在没有复写父类的方法的前提下,子类方法被执行了,这回引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务混乱,所以子类中方法的前置条件必须是超类中被复写的方法的前置条件相同或者更宽松。

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

首先请大家跟着我重新看一下第三条与第四条的标题:
3.覆盖或实现父类的方法时输入参数可以被放大
4.覆盖或实现父类的方法时输出结果可以被缩小

如果大家已经看出这两条有何不同了,那我们就可以继续向下进行了。首先请允许我摘抄书中关于第四条的定义:

父类的一个方法的返回值是一个类型T,子类的相同方法(重载或复写)的返回值为S,那么历史替换原则就要求S必须小于等于T,也就是说,要么S和T同一个类型,要么S是T的子类。

当然,凭啥他说不行就不行,我非要试试看:
在这里插入图片描述
被狠狠打脸(去掉@Override一样报错,有这个注解的方法一定是重写的,但是重写的方法不一定一定有@Override,大家可以自己删掉试试看)的同时,当然不能忘了看看这个报错究竟提示的是什么

getString()方法在Child中与getString()在Parent中冲突:试图返回不兼容的类型

好了,在按照规则写一遍
在这里插入图片描述

至于原理说实话到现在我也没有理解,书中也是用定义解释定义,而且就我这不太好用的脑子分析,好像解释的也不是第4条,而是又回去说了第3条。

网上搜了一下,依然没有找到浅显易懂的解释,甚至百度能查到的东西都很少(也可能是搜索的关键字不够关键)。所以只找到了一篇蝉蝉的博客Java学习笔记13—如何理解“子类重写父类方法时,返回值若为类类型,则必须与父类返回值类型相同或为其子类”中有所讲解,虽然我这笨脑子还是没看懂,希望对大家有所帮助吧。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值