什么是里氏代换原则
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新
的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
简单的理解为一个软件实体如果使用的是一个父类,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,软件里面,把父类都替换成它的子类,程序的行为没有变化。
但是反过来的代换却不成立,里氏代换原则(Liskov Substitution Principle):一个软件实体如果使用的是一个子类的话,那么它不能适用于其父类。
举个例子解释一下这个概念
先创建一个Person类
1 public class Person { 2 public void display() { 3 System.out.println("this is person"); 4 } 5 }
再创建一个Man类,继承这个Person类
1 public class Man extends Person { 2 3 public void display() { 4 System.out.println("this is man"); 5 } 6 7 }
运行一下
1 public class MainClass { 2 public static void main(String[] args) { 3 Person person = new Person();//new一个Person实例 4 display(person); 5 6 Person man = new Man();//new一个Man实例 7 display(man); 8 } 9 10 public static void display(Person person) { 11 person.display(); 12 } 13 }
可以看到
运行没有影响,符合一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类和子类对象的区别这句概念,这也就是java中的多态。
而反之,一个子类的话,那么它不能适用于其父类,这样,程序就会报错
1 public class MainClass { 2 public static void main(String[] args) { 3 Person person = new Person(); 4 display(person);//这里报错 5 6 Man man = new Man(); 7 display(man); 8 } 9 10 public static void display(Man man) {//传入一个子类 11 man.display(); 12 } 13 }
继续再举一个很经典的例子,正方形与长方形是否符合里氏代换原则,也就是说正方形是否是长方形的一个子类,
以前,我们上学都说正方形是特殊的长方形,是宽高相等的长方形,所以我们认为正方形是长方形的子类,但真的是这样吗?
从图中,我们可以看到长方形有两个属性宽和高,而正方形则只有一个属性边长
所以,用代码如此实现
1 //长方形 2 public class Changfangxing{ 3 private long width; 4 private long height; 5 6 public long getWidth() { 7 return width; 8 } 9 public void setWidth(long width) { 10 this.width = width; 11 } 12 public long getHeight() { 13 return height; 14 } 15 public void setHeight(long height) { 16 this.height = height; 17 } 18 }
1 //正方形 2 public class Zhengfangxing{ 3 private long side; 4 5 public long getSide() { 6 return side; 7 } 8 9 public void setSide(long side) { 10 this.side = side; 11 } 12 }
可以看到,它们的结构根本不同,所以正方形不是长方形的子类,所以长方形与正方形之间并不符合里氏代换原则。
当然我们也可以强行让正方形继承长方形
1 //正方形 2 public class Zhengfangxing extends Changfangixng{ 3 private long side; 4 5 public long getHeight() { 6 return this.getSide(); 7 } 8 9 public long getWidth() { 10 return this.getSide(); 11 } 12 13 public void setHeight(long height) { 14 this.setSide(height); 15 } 16 17 public void setWidth(long width) { 18 this.setSide(width); 19 } 20 21 public long getSide() { 22 return side; 23 } 24 25 public void setSide(long side) { 26 this.side = side; 27 } 28 }
这个样子,编译器是可以通过的,也可以正常使用,但是这样就符合里氏代换原则了吗,肯定不是的。
我们不是为了继承而继承,只有真正符合继承条件的情况下我们才去继承,所以像这样为了继承而继承,强行实现继承关系的情况也是不符合里氏代换原则的。
但这是为什么呢?,我们运行一下
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 test(changfangxing); 7 8 Changfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 test(zhengfangxing); 11 } 12 13 public static void test(Changfangxing changfangxing) { 14 System.out.println(changfangxing.getHeight()); 15 System.out.println(changfangixng.getWidth()); 16 } 17 }
结果:
我们忽然发现,很正常啊,为什么不可以,但是我们继续修改
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 resize(changfangxing); 7 8 Changfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 resize(zhengfangxing); 11 } 12 13 public static void test(Changfangxing changfangxing) { 14 System.out.println(changfangxing.getHeight()); 15 System.out.println(changfangxing.getWidth()); 16 } 17 18 public static void resize(Changfangxing changfangxing) { 19 while(changfangxing.getHeight() <= changfangxing.getWidth()) { 20 changfangxing.setHeight(changfangxing.getHeight() + 1); 21 test(changfangxing); 22 } 23 } 24 }
当长方形运行时,可以正常运行,而正方形则会造成死循环,所以这种继承方式不一定恩能够适用于所有情况,所以不符合里氏代换原则。
还有一种形式,我们抽象出一个四边形接口,让长方形和正方形都实现这个接口
1 public interface Sibianxing { 2 public long getWidth(); 3 public long getHeight(); 4 }
1 public class Changfangxing implements Sibianxing{ 2 private long width; 3 private long height; 4 5 public long getWidth() { 6 return width; 7 } 8 public void setWidth(long width) { 9 this.width = width; 10 } 11 public long getHeight() { 12 return height; 13 } 14 public void setHeight(long height) { 15 this.height = height; 16 } 17 }
1 package com.ibeifeng.ex3; 2 3 public class Zhengfangxing implements Sibianxing{ 4 private long side; 5 6 public long getHeight() { 7 return this.getSide(); 8 } 9 10 public long getWidth() { 11 return this.getSide(); 12 } 13 14 public void setHeight(long height) { 15 this.setSide(height); 16 } 17 18 public void setWidth(long width) { 19 this.setSide(width); 20 } 21 22 public long getSide() { 23 return side; 24 } 25 26 public void setSide(long side) { 27 this.side = side; 28 } 29 }
运行
1 public class MainClass { 2 public static void main(String[] args) { 3 Changfangxing changfangxing = new Changfangxing(); 4 changfangxing.setHeight(10); 5 changfangxing.setWidth(20); 6 test(changfangxing); 7 8 Zhengfangxing zhengfangxing = new Zhengfangxing(); 9 zhengfangxing.setHeight(10); 10 test(zhengfangxing); 11 } 12 13 public static void test(Sibianxing sibianxing) { 14 System.out.println(sibianxing.getHeight()); 15 System.out.println(sibianxing.getWidth()); 16 } 17 }
对于长方形和正方形,取width和height是它们共同的行为,但是给width和height赋值,两者行为不同,因此,这个抽象的四边形的类只有取值方法,没有赋值方法。上面的例子中那个方法只会适用于不同的子类,LSP也就不会被破坏。
注意事项
在进行设计的时候,尽量从抽象类继承,而不是从具体类继承。如果从继承等级树来看,所有叶子节点应当是具体类,而所有的树枝节点应当是抽象类或者接口。当然这个只是一个一般性的指导原则,使用的时候还要具体情况具体分析。