里式替换原则是什么?

里氏替换原则(Liskov Substitution Principle)的主旨是对于类 A 的子类 B,如果用子类 B 的对象代替父类 A 的对象,不会破坏原有程序的运行。这意味着子类必须与父类的行为保持兼容。替换原则是用于预测子类是否与代码兼容,以及是否能够与父类对象协作的一组检查。
遵循里式替换原则对子类的形式有若干个要求:

  • 子类方法的参数类型必须与其父类的参数类型相匹配或更加抽象。
  • 子类方法的返回值类型必须与超类方法的返回值类型或是其子类别相匹配。
  • 子类中的方法不应抛出基础方法预期之外的异常类型。
  • 子类不应该加强其前置条件。
  • 子类不能削弱其后置条件。
  • 超类的不变量必须保留。
  • 子类不能修改超类中私有成员变量的值。

这些规则有部分本身在 Java 中已经内置限制,如有违反,则无法通过编译。

案例

这个原则有点不易理解,我们还是用一个简单的案例来帮助我们理解它。假设现在有一个文档管理系统,我们定义了一个 Document 类来表示文档,同时定义了一个 open 方法表示打开文档,save 方法表示保存文档。

  1. public class Document {
  2. private String filename;
  3. // constructor, getters and setters
  4. public byte[] open() {
  5. // Our code for opening the document
  6. }
  7. public boolean save(byte[] data) throws IOException {
  8. // Our code for saving the document
  9. }
  10. }

对于只读文档,保存操作是没有意义的,save 方法在被调用时抛出一个 UnsupportedOperationException 异常:

  1. public class ReadOnlyDocument extends Document {
  2. @Override
  3. public boolean save(byte[] data) throws IOException {
  4. throw new UnsupportedOperationException("Unable to save read-only file.");
  5. }
  6. }

然而父类的方法是没有这个限制的,这意味着如果我们在保存文档时没有先检查它的类型,客户端代码将会出错。而如果我们检查文档类型的话,代码也将违反开闭原则,因为客户端代码将依赖于具体的文档类。
我们可以重新设计类的层次结构来解决这个问题:一个子类必须扩展其父类的行为。因此只读文档变成了层次结构中的基类,可写文件则变成了子类,对基类进行扩展并添加了保存操作。

  1. public class Document {
  2. private String filename;
  3. // constructor, getters and setters
  4. public byte[] open() {
  5. // Our code for opening the document
  6. return null;
  7. }
  8. }
  9. public class WritableDocument extends Document {
  10. public boolean save(byte[] data) throws IOException {
  11. // Our code for saving the document
  12. return true;
  13. }
  14. }

案例源码

可在 GitHub 上查看上述案例的完整代码。

参考资料

以下是本文参考的资料: