Kotlin 核心编程 (49):面向对象 3.2.1

阅读数:3 2019 年 12 月 25 日 15:35

Kotlin核心编程(49):面向对象 3.2.1

(限制修饰符)

内容简介
本书不是一本简单介绍 Kotlin 语法应用的图书,而是一部专注于帮助读者深入理解 Kotlin 的设计理念,指导读者实现 Kotlin 高层次开发的实战型著作。书中深入介绍了 Kotlin 的核心语言特性、设计模式、函数式编程、异步开发等内容,并以 Android 和 Web 两个平台为背景,演示了 Kotlin 的实战应用。
全书共 13 章,分为 4 个部分:
热身篇—Kotlin 基础(第 1~2 章),简单介绍了 Kotlin 设计哲学、生态及基础语法,其中包括 Kotlin 与 Scala、Java 之间的关联与对比,以及 Kotlin 的类型声明的特殊性、val 和 var 的使用、高阶函数的使用、面向表达式编程的使用、字符串的定义与操作等内容;
下水篇—Kotlin 核心(第 3~8 章),深入介绍了面向对象、代数数据类型、模式匹配、类型系统、Lambda、集合、多态、扩展、元编程等 Kotlin 开发核心知识,这是本书的重点,其中涉及很多开发者特别关心的问题,比如多继承问题、模式匹配问题、用代数数据类型抽象业务问题、泛型问题、反射问题等。
潜入篇—Kotlin 探索(第 9~11 章),探索 Kotlin 在设计模式、函数式编程、异步和并发等编程领域的应用,其中包括对 4 大类设计模式、Typeclass 实现、函数式通用结构设计、类型替代异常处理、共享资源控制、CQRS 架构等重点内容的深入剖析;
遨游篇—Kotlin 实战(第 12~13 章),着重演示了 Kotlin 在 Android 和 Web 平台的实战案例,其中涉及架构方式、单向数据流模型、解耦视图导航、响应式编程、Spring 5 响应式框架和编程等内容。

当你想要指定一个类、方法或属性的修改或者重写权限时,你就需要用到限制修饰符。我们知道,继承是面向对象的基本特征之一,继承虽然灵活,但如果被滥用就会引起一些问题。还是拿之前的 Bird 类举个例子。Shaw 觉得企鹅也是一种鸟类,于是他声明了一个 Penguin 类来继承 Bird。

复制代码
open class Bird {
open fun fly() {
println("I can fly.")
}
}
class Penguin : Bird() {
override fun fly() {
println("I can't fly actually.")
}
}

首先,我们来说明两个 Kotlin 相比 Java 不一样的语法特性:

  • Kotlin 中没有采用 Java 中的 extends 和 implements 关键词,而是使用“:”来代替类的继承和接口实现;
  • 由于 Kotlin 中类和方法默认是不可被继承或重写的,所以必须加上 open 修饰符。

其次,你肯定注意到了 Penguin 类重写了父类中的 fly 方法,因为虽然企鹅也是鸟类,但实际上它却不会飞。这个其实是一种比较危险的做法,比如我们修改了 Bird 类的 fly 方法,增加了一个代表每天能够飞行的英里数的参数:miles。

复制代码
open class Bird {
open fun fly(miles: Int) {
println("I can fly ${miles} miles daily.")
}
}

现在如果我们再次调用 Penguin 的 fly 方法,那么就会出错,错误信息提示 fly 重写了一个不存在的方法。

复制代码
Error:(8, 4) 'fly' overrides nothing

事实上,这是我们日常开发中错误设计继承的典型案例。因为 Bird 类代表的并不是生物学中的鸟类,而是会飞行的鸟。由于没有仔细思考,我们设计了错误的继承关系,导致了上述的问题。子类应该尽量避免重写父类的非抽象方法,因为一旦父类变更方法,子类的方法调用很可能会出错,而且重写父类非抽象方法违背了面向对象设计原则中的“里氏替换原则”。

什么是里氏替换原则?

  • 对里氏替换原则通俗的理解是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下 4 个设计原则:
  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
  • 子类可以增加自己特有的方法;
  • 当子类的方法实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松;
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

然而,实际业务开发中我们常常很容易违背里氏替换原则,导致设计中出问题的概率大大增加。其根本原因,就是我们一开始并没有仔细思考一个类的继承关系。所以《Effective Java》也提出了一个原则:“要么为继承做好设计并且提供文档,否则就禁止这样做”。

  1. 类的默认修饰符:final

Kotlin 站在前人肩膀上,吸取了它们的教训,认为类默认开放继承并不是一个好的选择。所以在 Kotlin 中的类或方法默认是不允许被继承或重写的。还是以 Bird 类为例:

复制代码
class Bird {
val weight: Double = 500.0
val color: String = "blue"
val age: Int = 1
fun fly() {}
}

这是一个简单的类。现在我们把它编译后转换为 Java 代码:

复制代码
public final class Bird {
private final double weight = 500.0D;
private final String color = "blue";
private final int age = 1;
public final double getWeight() {
return this.weight;
}
public final String getColor() {
return this.color;
}
public final int getAge() {
return this.age;
}
public final void fly() {
}
}

我们可以发现,转换后的 Java 代码中的类,方法及属性前面多了一个 final 修饰符,由它修饰的内容将不允许被继承或修改。我们经常使用的 String 类就是用 final 修饰的,它不可以被继承。在 Java 中,类默认是可以被继承的,除非你主动加 final 修饰符。而在 Kotlin 中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是之前例子中的 open。

现在,我们给 Bird 类加上 open 修饰符:

复制代码
open class Bird {
val weight: Double = 500.0
val color: String = "red"
val age: Int = 1
fun fly() {}
}

大家可以想象一下,这个类被编译成 Java 代码应该是怎么样的呢?其实就是我们最普通定义 Java 类的代码:

复制代码
public class Bird {
...
}

此外,也正如我们所见,如果我们想让一个方法可以被重写,那么也必须在方法前面加上 open 修饰符。这一切似乎都是与 Java 相反着的。那么,这种默认 final 的设计真的就那么好吗?

  1. 类默认 final 真的好吗

一种批评的声音来自 Kotlin 官方论坛,不少人诟病默认 final 的设计会给实际开发带来不便。具体表现在:

  • 与某些框架的实现存在冲突。如 Spring 会利用注解私自对类进行增强,由于 Kotlin 中的类默认不能被继承,这可能导致框架的某些原始功能出现问题。
  • 更多的麻烦还来自于对第三方 Kotlin 库进行扩展。就统计层面讨论,Kotlin 类库肯定会比 Java 类库更倾向于不开放一个类的继承,因为人总是偷懒的,Kotlin 默认 final 可能会阻挠我们对这些类库的类进行继承,然后扩展功能。

Kotlin 论坛甚至举行了一个关于类默认 final 的喜好投票,略超半数的人更倾向于把 open 当作默认情况。相关帖子参见: https://discuss.kotlinlang.org/t/classes-final-by-default/166

以上的反对观点很有道理。下面我们再基于 Kotlin 的自身定位和语言特性重新反思一下这些观点。

1)Kotlin 当前是一门以 Android 平台为主的开发语言。在工程开发时,我们很少会频繁地继承一个类,默认 final 会让它变得更加安全。如果一个类默认 open 而在必要的时候忘记了标记 final,可能会带来麻烦。反之,如果一个默认 final 的类,在我们需要扩展它的时候,即使没有标记 open,编译器也会提醒我们,这个就不存在问题。此外,Android 也不存在类似 Spring 因框架本身而产生的冲突。

2)虽然 Kotlin 非常类似于 Java,然而它对一个类库扩展的手段要更加丰富。典型的案例就是 Android 的 Kotlin 扩展库 android-ktx。Google 官方主要通过 Kotlin 中的扩展语法对 Android 标准库进行了扩展,而不是通过继承原始类的手段。这也揭示了一点,以往在 Java 中因为没有类似的扩展语法,往往采用继承去对扩展一个类库,某些场景不一定合理。相较而言,在 Kotlin 中由于这种增强的多态性支持,类默认为 final 也许可以督促我们思考更正确的扩展手段。

除了扩展这种新特性之外,Kotlin 中的其他新特性,比如 Smart Casts 结合 class 的 final 属性也可以发挥更大的作用。

Kotlin 除了可以利用 final 来限制一个类的继承以外,还可以通过密封类的语法来限制一个类的继承。比如我们可以这么做:

复制代码
sealed class Bird {
open fun fly() = "I can fly"
class Eagle : Bird()
}

Kotlin 通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。但这种方式有它的局限性,即它不能被初始化,因为它背后是基于一个抽象类实现的。这一点我们从它转换后的 Java 代码中可以看出:

复制代码
public abstract class Bird {
@NotNull
public String fly() {
return "I can fly";
}
private Bird() {
}
// $FF: synthetic method
public Bird(DefaultConstructorMarker $constructor_marker) {
this();
}
public static final class Eagle extends Bird {
public Eagle() {
super((DefaultConstructorMarker) null);
}
}
}

密封类的使用场景有限,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用。有关模式匹配的内容将会在下一章讲解。

总的来说,我们需要辩证地看待 Kotlin 中类默认 final 的原则,它让我们的程序变得更加安全,但也会在其他场合带来一定的不便。最后,关于限制修饰符,还有一个 abstract。abstract 大家也不陌生,它若修饰在类前面说明这个类是抽象类,修饰在方法前面说明这个方法是一个抽象方法。Kotlin 中的 abstract 和 Java 中的完全一样,这里就不过多阐述了。Kotlin 与 Java 的限制修饰符比较如表 3-1 所示。

表 3-1 Kotlin 与 Java 的限制修饰符比较
修饰符 含义 与 Java 比较
open 允许被继承或重写 相当于 Java 类与方法的默认情况
abstract 抽象类或抽象方法 与 Java 一致
final 不允许被继承或重写(默认情况) 与 Java 主动指定 final 的效果一致

Kotlin核心编程(49):面向对象 3.2.1

购书地址 https://item.jd.com/12519581.html?dist=jd

评论

发布