概述
JAVA设计原则–里氏替换原则(LSP原则)
为什么要用里氏替换原则?:
为了优化继承所带来的缺点,使得继承的优点发挥到最大,而同时减少缺点带来的麻烦。
继承的优缺点:
优点:
1. 代码共享,减少创建类的工作量,每个子类都拥有父类的属性和方法
2. 提高代码的重用性(子类可以使用父类的属性和方法)
3. 子类可以在父类的基础上进行拓展(重写父类的方法,实现自己的逻辑)(很多开源框架的扩展接口都是通过继承父类来实现的)。
缺点:
1. 继承是具有侵入性的。也就是只要继承,就必须拥有父类的所有属性和方法
2. 降低了子类的灵活性。同强增加了耦合性。(因为子类具有父类的属性和变量,所以,在修改父类的属性和方法时,需要考虑子类的修改,而这种修改如果没有规范,可能需要大段的代码重构)
里氏替换原则的规范(定义):
1、所有引用父类的地方都必须能透明地使用其子类的对象。
(只要哪里使用了父类,那么他的所有子类也必须能使用,替换子类不会发生任何错误或异常,调用者不需要知道是子类还是父类。)
2、子类出现的地方,父类不一定能适用。
里氏替换原则规范的含义:
1.子类必须完全实现父类的方法
例: 在平常编写代码时,定义接口,然后编写实现类,而在调用时,直接调用接口方法(高层抽象)(其他场景下还有抽象类这种情况),而不去调用具体的实现了,其实这里已经使用了里氏替换原则。
例:士兵射击的场景—–枪(本篇中的例子均来自<<设计模式之禅>>)
枪有很多种类,具体士兵使用什么枪,得等到调用的时候才知道,所以需要我们对枪进行抽象:
枪支的抽象类:
public abstract class AbstractGun {
//定义模板射击方法 具体让子类去实现
public abstract void shoot();
// 枪的形状
public void shape() { // 枪的形状};
// 枪的声音
public void voice( // 枪的声音);
}
// 手枪
public class Handgun extends AbstractGun {
// 手枪的射击方法
@Override
public void shoot() {
System.out.println("手枪射击");
}
}
// 步枪
public class Rifle extends AbstractGun {
// 步枪的射击
@Override
public void shoot() {
System.out.println("步枪射击");
}
}
// 机枪
public class MachineGun extends AbstractGun {
// 机枪
@Override
public void shoot() {
System.out.println("机枪扫射");
}
}
有了枪支,还需要士兵去使用(调用)枪支:
// 士兵
public class Soldier {
// 定义士兵的枪支
private AbstractGun gun;
// 给士兵枪
public void setGun(AbstractGun _gun) {
this.gun = _gun;
}
// 射击敌人
public void killEnemy() {
System.out.println("士兵开始杀敌人");
gun.shoot();
}
}
注意: 在Soldier类中,调用枪类时,调用了顶级父类AbstractGun ,这符合了里氏替换原则(LSP):
在类中调用其他类时,务必使用父类或接口(高层抽象),如果不能使用父类或接口,则说明类的设计已经违背了原则。
士兵有了,枪支有了,之后就需要在实际中(具体场景)去用具体的枪射击敌人:
public class client {
public static void main(String[] args) {
// 产生士兵
Soldier soldier = new Soldier();
// 给士兵枪支 这里给了步枪 如果要使用其他的枪,传入其他具体的子类即可
soldier.setGun(new Rifile);
// 士兵射击敌人
soldier.killEnemy();
}
}
注意:当出现特殊的子类,并且无法应用在父类的场景下时,应当对子类进行抽象,建立一个独立的父类,并将枪支的抽象类与特殊的子类的抽象类建立委托关系。
// 因为玩具枪不满足场景(不能杀敌人),所以得独立建立父类,然后两个父类下的子类各自延展
public abstract class AbstractToy {
// 与枪支的抽象类建立委托关系 将声音、形状等等一些特性都委托给AbstractGun处理
private AbstractGun gun;
// 委托给AbstractGun处理
public void shape() {
System.out.println("枪的形状");
gun.shape();
};
// 委托给AbstractGun处理
public void voice() {
System.out.println("枪射击的声音");
gun.voice();
}
}
public class ToyGun extends AbstractToy {
// 玩具枪形状
super.shape();
// 玩具枪声音
super.shape();
}
注意: 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系替代继承。
2.子类可以存在属于自己的属性和方法
里氏替换原则可以正着用,但是不能反着用(在子类出现的地方,父类未必能使用。)
public class AUG extends Rifile {
// 狙击枪带望远镜
public void zoomOut() {
System.out.println("通过望远镜观察敌人");
}
//
public void shoot() {
System.out.println("AUG射击。。。。。。");
}
}
// AUG狙击手
public class Snipper {
public void killEnemy(Aug aug) {
// 观察敌人
aug.zoomOut();
// 开始射击
anu.shoot();
}
}
// 客户端调用(使用子类的场景)
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setRile(new AUG());
snipper.killEnemy();
}
}
// 客户端调用(使用父类替代子类)
public class Client {
public static void main(String[] args) {
Snipper snipper = new Snipper();
snipper.setRile((AUG) new Rifle());
snipper.killEnemy();
}
}
编写上面那段代码,会发现在运行期间会抛出java.lang.ClassCastException异常,也就是说,向下转型是不安全的,从里氏替换这个原则上来看,就是子类出现的地方,父类不一定能使用。
3.重写或者实现父类的方法时,输入参数可以被放大
例:
// 返回Collection集合类型
public class Father {
public Collection doSomething(HashMap map) {
System.out.println("父类被执行...");
return map.values();
}
}
// 子类返回Collection集合类型
public class Son extends Father {
// 放大输入参数类型
public Collection doSomething(Map map) {
System.out.println("子类被执行");
return map.values();
}
}
// 场景类
public class Client {
public static void invoker() {
// 父类存在的地方,子类可以替代
Father father = new Father();
HashMap map = new HashMap();
father.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
// 场景类
public class Client {
public static void invoker() {
Son son = new Son();
HashMap map = new HashMap();
son.doSomething(map);
}
public static void main(String[] args) {
invoker();
}
}
// 然后会发现,上面两种场景下执行结果相同。
注意: 在上面例子中,子类和父类的方法名相同,但是参数列表不同(方法参数类型不同),所以,这不是重写,而是重载,因为继承是让子类拥有父类的属性和方法,所以在子类中,有两个方法,方法名相同,但是参数列表不同,所以是重载。
父类的参数类型是HashMap,子类的参数类型是Map,说明参数类型的范围被扩大了,当传入的参数是HashMap时,会发现子类代替父类执行(因为继承了父类的方法),而真正的子类方法不会被调用。而子类想执行方法,必须重写或重载父类的方法,这样做是正确的。
因为如果父类参数的类型范围大于子类的话,那么父类出现的地方,子类未必可以使用,可能导致程序出错。
总结: 子类的参数的范围类型必须大于等于父类的参数的范围类型
4.重写或者实现父类的方法时输出结果可以被缩小
父类的一个方法的返回值的类型是T,子类的相同方法(重载或重写)的返回类型是S,那么按照里氏替换原则,S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类 :
第一种情况: 重写:
父类和子类的同名方法的输入参数是相同的,放个方法的范围值S小于等于T,这个是重写的要求(这是为了向上转型;既然子类重写了父类的方法,有时候就需要用父类对象引用来调用子类重写的方法)
第二种情况:重载
要求方法的输入参数类型或数量不相同,根据里氏替换原则,就是子类的参数范围要大于或等于父类的参数范围,也就是说,你写的方法是不会被调用的。
最后
以上就是务实羊为你收集整理的java设计原则--里氏替换原则JAVA设计原则–里氏替换原则(LSP原则)为什么要用里氏替换原则?:继承的优缺点:里氏替换原则的规范(定义):里氏替换原则规范的含义:的全部内容,希望文章能够帮你解决java设计原则--里氏替换原则JAVA设计原则–里氏替换原则(LSP原则)为什么要用里氏替换原则?:继承的优缺点:里氏替换原则的规范(定义):里氏替换原则规范的含义:所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复