里氏替换原则
定义
Liskov Substitution Principle
里氏替换原则:一种面向对象编程的设计原则,要求子类对象能够替换其父类对象,且不影响程序的正确性。
在该定义中需要抓住两个重点:
- 子类必须能够替换他们的基类,意味着子类必须保持与父类行为兼容,在重写一个方法时,你要对基类行为进行扩展,而不是将其完全替换
- 继承表达类型抽象
上述解释还是概念比较强,我们来一个形象的比喻,里氏替换原则可以理解为“青出于蓝而胜于蓝”,子类完美继承父类接口设计的约定,并做了增强 而不是改变父类的设计初衷。
多态
定义
父类的引用可以指向子类的对象
笔者刚接触里氏替换原则的时候,就有点与多态区分不开。 多态 是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。 里氏替换原则 是一种针对子类和父类关系的设计原则,用来指导继承关系中的子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏 原有程序的正确性。
public class ListDemo {
public static void main(String[] args) {
// 父类的引用指向子类的对象
List arrayList = new ArrayList();
List linkedList = new LinkedList();
addV(arrayList,123);
addV(linkedList,123);
}
/***
* addV方法第一个参数为List 是一个接口,支持传入任何List接口的子类。这家就是多态
* @param list
* @param val
*/
public static void addV(List list, Object val) {
list.add(val);
}
}
分析
我们还是以JDK的集合ArrayList、LinkedList和AbstractSequentialList源码为例,来分析一下里氏替换原则
- ArrayList类结构
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{...}
- LinkedList类结构
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{...}
- AbstractSequentialList类结构
public abstract class AbstractSequentialList<E> extends AbstractList<E> {...}
我们可以看到,ArrayList和LinkedList都属于List接口的子类, 都属于AbstractList抽象类的子类(虽然LinkedList中间还有一个AbstractSequentialList的父类, 但在整个继承链上依然是AbstractList的子类)。现在我们基于ArrayList和LinkedList书写以下代码来展示“替换”的精妙之处。
public class ListDemo {
public static void main(String[] args) {
// 父类的引用指向子类的对象
List arrayList = new ArrayList();
List linkedList = new LinkedList();
addV(arrayList,123);
addV(linkedList,123);
}
/***
* addV方法第一个参数为List 类型,父类类型,支持传入任何List接口的子类ArrayList和LinkedList。此处体现里氏替换原则
* @param list
* @param val
*/
public static void addV(List list, Object val) {
list.add(val);
}
}
然而,我们都知道ArrayList和LinkedList的区别,LinkedList中有自己特有的属性和方法,比如LinkedList中有addFirst而ArrayList则没有 因此也从侧面印证了里氏替换原则的核心,子类并不改变父类的设计的初衷,子类只是完美继承父类的设计初衷并做增强。
哪些代码会明显违背里氏替换原则
- 子类违背父类声明要实现的功能 比如:父类中提供一个方法是按照创建时间排序,子类重写后,按照更新时间排序,这就违背了里氏替换原则
- 子类违背父类对输入、输出、异常的约定 比如:父类中一个函数的约定是运行出错返回null,获取数据为空时返回空集合,子类重写函数之后,运行出错返回异常,获取不到数据返回null。这就违背了里氏替换
- 子类违背父类注释中所罗列的任何特殊说明 比如:父类中定义提现函数,注释中这么写的"用户提现金额不能超过账户余额",而子类重写后,针对VIP账户实现了透支提现功能 也就是说提现金额可以大于账户余额。这就不符合里氏替换原则
【例子1】违反替换原则的文档类层次结构例子
Document接口
public interface Document {
public void open();
public void save();
}
ReadOnlyDocument实现 Document子类
// 只读文件中的save调用会抛出异常,然而在父类的save方法中则没有此限制,违反了父类的约定,即违反了里氏替换原则
public class ReadOnlyDocument implements Document {
@Override
public void open() {
}
@Override
public void save() {
throw new RuntimeException("无法保存");
}
}
Project类
// 此处代码也违反了开闭原则,因为客户端代码将依赖于具体的文档类。如果你引入了新的文档子类,则需要修改客户端代码才能对其进行支持。
public class Project {
private List<Document> documentList;
public void openAll() {
for (Document doc:documentList) {
doc.open();
}
}
public void saveAll() {
for (Document doc:documentList) {
if (doc instanceof ReadOnlyDocument) {
} else {
doc.save();
}
}
}
}
上述问题如何解决? 当把只读文档类作为层次结构中的基类后,这个问题得到了解决。
你可以通过重新设计类层次结构来解决这个问题:一个子类必须扩展其超类的行为,因此只读文档变成了层次结构中的基类。 可写文件现在变成了子类,对基类进行扩展并添加了保存行为。 Document接口
// 把只读文档类作为层次结构中的基类
public interface Document {
public void open();
}
WritableDocument实现 Document子类
public class WritableDocument implements Document {
@Override
public void open() {
}
public void save() {
}
}
Project类
public class Project {
private List<WritableDocument> writableList;
private List<Document> allList;
public void openAll() {
for (Document doc:allList) {
doc.open();
}
}
public void saveAll() {
for (Document doc:writableList) {
doc.save();
}
}
}
总结
通过上面的学习和分析,我们再来看一下里氏替换更具有意义的定义:按照协议来设计,子类在设计的时候,要遵守父类的行为约定/协议 。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。行为约定包括: 函数声明要实现的功能;对输入、输出、异常的约定;注释中所罗列的特殊说明。