从规约到 Liskov 替换原则:深入理解子类型多态

规约(Specifications)

规约的概念

程序的作用在于交流

  • 代码中蕴含的 " 设计决策 ":与编译器交流
  • 注释形式的 " 设计决策 ":与自己和别人交流

那么什么是规约呢?

  • 静态类型声明(static type declarations)是一种规约,可据此进行静态类型检查(static type checking);
  • 方法前的注释也是一种规约,但需人工判定其是否满足。

本文后续所指的规约(specfications,下文简称 spec),指的便是方法前注释形式的 " 设计决策 "

以 JDK List 接口中的 E remove(int index) 为例:

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
 E remove(int index) {

写在方法前的 /** … */ 中的内容,便是 spec。

书写规约的意义

规约是服务端客户端之间达成的一致

  • 对于服务端开发者而言:没有规约,无法编程;即使写出来,也不知道对错;
  • 对于客户端开发者而言:无需阅读调用函数的代码,只需理解 spec 即可。

Spec 给 " 供需双方 " 都确定了责任,在调用的时候双方都要遵守。责任,即前置条件后置条件,下文会对此进行详细解读。

规约的组成

Spec 是供需双方之间达成的契约,如果前置条件满足了,后置条件必须满足:

  • 前置条件(precondition):对客户端的约束,使用方法时必须满足的条件;
  • 后置条件(postcondition):对服务端的约束,方法结束时必须满足的条件。

Spec 包含了以下几层含义:

  • 前置条件满足,则后置条件必须满足;
  • 前置条件不满足,则方法可做任何事情。

继续以 JDK List 接口中的 E remove(int index) 为例:

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).
 *
 * @param index the index of the element to be removed
 * @return the element that was removed from the list
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
 E remove(int index) {

其中:

  • 前置条件对应于 @param
  • 后置条件对应于 @return@throws

规约的强度

规约强度的概念来源于规约替换的想法,我们希望强度较高的规约能够替换强度较低的规约。于是,便引出了规约强度的概念。

设两个规约 S 1 , S 2 S_{1}, S_{2} S1,S2,规约 S 2 S_{2} S2 的强度大于等于 S 1 S_{1} S1,如果:

  • S 2 S_{2} S2 的前置条件较弱(i.e. Precondition ( S 2 ) ≤ Precondition ( S 1 ) \textrm{Precondition}(S_{2}) \leq \textrm{Precondition}(S_{1}) Precondition(S2)Precondition(S1)
  • S 2 S_{2} S2 的后置条件较强(i.e. Postcondition ( S 2 ) ≥ Postcondition ( S 1 ) \textrm{Postcondition}(S_{2}) \geq \textrm{Postcondition}(S_{1}) Postcondition(S2)Postcondition(S1)

规约的强度对于后续分析子类型多态Liskov 替换原则十分重要。

Liskov 替换原则与子类型多态

多态的类型

  • 特殊多态:(Ad hoc polymorphism): 体现在功能重载(overload)
  • 参数化多态:(Parametric polymorphism): 体现在泛型(generics)
  • 子类型多态:(Subtyping polymorphism): 不同类型的对象可以统一的处理而无需区分

本文重点讨论子类型多态。

类型与子类型

  • 类型(type):一些值的集合(a set of values)
  • 子类型(subtype):子类型是超类型的子集(a subset of the supertype)

例如,Java 中,ArrayListLinkedList 都是 List 的子类型。

我们从两种角度来理解子类型,例如 “B is a subtype of A”:

  • 从自然语言的角度来看:“every B is an A”
  • 从规约(spec)的角度来看:“every B satisfies the specification for A”,更进一步,B 是 A 的子类型,当且仅当 B 的 spec 不比 A 弱。

Liskov 替换原则

Liskov 替换原则要求:如果 S 是 T 的子类型,则类型 T 的对象可以替换为类型 S 的对象(即类型 T 的对象可以替换为子类型 S 的任何对象),而不改变 T 的任何所需属性。事实上,也就意味着 S 的 spec 不比 T 弱。

具体地,对于子类型重写(override)的方法 spec 要求如下:

  • @param:相同类型或者符合逆变
  • @return:相同类型或者符合协变
  • @throws:不能抛出额外的异常,抛出相同或者符合协变的异常
协变(co-variance)

协变(co-variance),意味着子类型方法要强化后置条件(返回值与异常),进而强化 spec。

例如,父类方法 T.a() 返回值类型为 Object,子类方法 S.a() override 时,可以返回 Object 的子类型 String,这样便强化了后置条件(返回值)。

class T {
    Object a() {}
}

class S extends T {
    @Override
    String a() {}
}

再例如,父类方法 T.b() 异常类型为 Exception,子类方法 S.b() override 时,可以抛出 Exception 的子类型 IOException,这样便强化了后置条件(异常)。当然,S 的子类方法 U.b() 可以不抛出异常,进而也强化了后置条件(异常)。

class T {
    void b() throws Exception {}
}

class S extends T {
    @Override
    void b() throws IOException {}
}

class U extends S {
    @Override
    void b() {}
}
逆变(contra-variance)

逆变(contra-variance),与协变相反,意味着子类型方法要弱化前置条件(参数),进而强化 spec。

例如,父类方法 T.c() 参数类型为 String,子类方法 S.c() override 时,可以返回 String 的父类型 Object,这样便弱化了前置条件(参数)。

class T {
    void c(String s) {}
}
class S extends T {
    @Override
    void c(Object s) {}
}

Note
但是 Java 目前并不支持逆变,这种情形会被当做overload,而非override
因此,Java 继承机制中,子类型方法 override 时,参数类型必须与父类型方法一致。

  • 32
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值