里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
里氏替换原则在SOLID这五个设计原则中是比较特殊的存在:
里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。
里氏替换原则最初由Barbara Liskov在1987年的一次学术会议中提出,而真正正式发表是在1994年,Barbara Liskov 和 Jeannette Wing发表的一篇学术论文《A behavioral notion of subtyping》.
里氏替换原则在1994年Barbara Liskov 和 Jeannette Wing发表论文中的描述是:
If S is a declared subtype of T, objects of type S should behave as objects of type T are expected to behave, if they are treated as objects of type T
从字面上翻译:如果S是T的子类型,对于S类型的任意对象,如果将他们看作是T类型的对象,则对象的行为也理应与期望的行为一致。
而另一种关于里氏替换原则的描述为Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。
不管是Barbara Liskov论文中的表述,还是Robert Martin的解读,都是比较抽象的表达。要理解里氏替换原则,其实就是要理解两个问题:
替换的前提是面向对象语言所支持的多态特性,同一个行为具有多个不同表现形式或形态的能力。以JDK的集合框架为例,List
接口的定义为有序集合,List
接口有多个派生类,比如大家耳熟能详的ArrayList
, LinkedList
。那当某个方法参数或变量是List
接口类型时,既可以是ArrayList
的实现, 也可以是LinkedList
的实现,这就是替换。
举个简单的例子:
public String getFirst(List values) {
return values.get(0);
}
对于getFirst
方法,接受一个List
接口类型的参数,那既可以传递一个ArrayList
类型的参数:
List values = new ArrayList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);
又可以接收一个LinkedList
参数:
List values = new LinkedList<>();
values.add("a");
values.add("b");
String firstValue = getFirst(values);
在不了解派生类的情况下,仅通过接口或基类的方法,即可清楚的知道方法的行为,而不管哪种派生类的实现,都与接口或基类方法的期望行为一致。或者说接口或基类的方法是一种契约,使用方按照这个契约来使用,派生类也按照这个契约来实现。这就是与期望行为一致的替换。继续以上节中的例子说明:
public String getFirst(List values) {
return values.get(0);
}
对于getFirst
方法,接收List
类型的参数,而List
类型的get
方法返回特定位置的元素,对于本例即为第一个元素。这些是不依赖派生类的知识的。所以对于上节中的示例,不管是ArrayList
类型的实现,还是LinkedList
的实现,getFirst
方法最终的返回值是一样的。这就是与期望行为一致的替换。
从直观上可能觉得派生类对象可以在替换其基类对象是理所当然的,但会有出现一些场景有意无意地违反了里氏替换原则。
还以JDK的集合框架为例,如果自定义一个List的派生类,如下:
class CustomList extends ArrayList {
@Override
public T get(int index) {
throw new UnsupportedOperationException();
}
}
仅重写get方法,throw一个UnsupportedOperationException
,因为List
接口关于get方法的描述,仅会抛出IndexOutOfBoundsException
, throw UnsupportedOperationException
的行为并不是基类所期望的,即违反了里氏替换原则,
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException if the index is out of range
* ({@code index < 0 || index >= size()})
*/
E get(int index);
同样,如果自定义另一个List的派生类,如下:
class CustomList extends ArrayList {
@Override
public T get(int index) {
if (index >= size()){
return null;
}
return get(index);
}
}
仅重写get方法,当输入index大于当前list的size时,返回null,而不抛出IndexOutOfBoundsException
, 因为List
接口关于get方法的描述,当index超出范围时抛出IndexOutOfBoundsException
,所以改变了基类方法的语义,即违反了里氏替换原则。
当我们违反了这一原则会带来有一些危害:
谈到如何避免,当然要基于里氏替换原则的定义,与期望行为一致的替换
。