Kotlin为什么将class设计成final类型

Kotlin开发人员为什么将类设计成final类型?”

继承是所有OOP语言和Java语言不可缺少的组成部分。当创建一个类时,总是在继承,除非已明确指出要从其他类继承,否则就是隐式地从Java的标准根类Object进行继承。

然而当我们初学Kotlin,第一次写下再熟悉不过的继承语法,编辑器往往会出乎意料的提示你,你要继承的类是final类型,是否需要使它open?默认不能继承???需要加open关键字???

带着疑问,我们打开Kotlin官方文档。在类与继承部分中提到:Kotlin类是final的,他们不能被继承。要使一个类可被继承,请使用open关键字标记它。

open class Base //该类开放继承

那么Kotlin为什么要将类设计成默认不可继承呢?

我们先来聊聊Java中的继承。

在面向对象的语言中,继承是必不可少、非常优秀的语言机制,它有如下优点:

然而自然界的所有事物都是优点和缺点并存的。继承的缺点如下:

继承是实现代码重用的有力手段,但它并非永远是完成这项工作的最佳工具。使用不当会导致软件变得很脆弱。在包的内部使用继承是非常安全的,在那里,子类和超类的实现都处在同一个程序员的控制下。对于专门为了继承而设计、并且具有很好的文档说明的类来说,使用继承也是非常安全的。然而,对普通的具体类进行跨越包边界的继承,则是非常危险的。

与方法调用不同的是,继承打破了封装性。换句话说,子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。因而,子类必须要跟着其超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文档说明。

幸运的是,有一种办法可以避免前面提到的所有问题。

不用扩展现有的类,而是在新类中增加一个私有域,它引用现有类的一个实例。这种设计被称为“复合”,因为现有的类变成了新类的一个组件。新类中的每个实例方法都可以调用被包含的现有类实例中对应的方法,并返回它的结果。这被称为转发,新类中的方法被称为转发方法。这样得到的类将会非常稳固,它不依赖现有类的实现细节。即使现有的类添加了新的方法,也不会影响新的类。

所以继承和复合该怎样选择呢?

只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该扩展类A。如果你打算让类B扩展类A,那就该问问自己:每个B确实也是A吗?如果你不能够确定这个问题的答案是肯定的,那么B就不该扩展A。如果答案是否定的,通常情况下,B应该包含A的一个私有实例,并且暴露一个较小的、较简单的API:A本质上不是B的一部分,只是它的实现细节而已。

Java平台类库中,有许多明显违反这条原则的地方。例如,栈(stack)并不是向量(vector),所以Stack不应该扩展Vector。同样地,属性列表也不是散列表,所以Properties不应该扩展Hashtable。在这两种情况下,复合模式才是恰当的。

如果在适合使用复合的地方使用了继承,则会不必要地暴露实现细节。这样得到的API会把你限制在原始的实现上,永远限定了类的性能。更为严重的是,由于暴露了内部的细节,客户端就有可能直接访问这些内部细节。这样至少会导致语义上的混淆。例如,如果p指向Properties实例,那么p.getProperty(key)就有可能产生与p.get(key)不同的结果:前者考虑了默认的属性表,而后者是继承自Hashtable的,它则没有考虑默认属性列表。最严重的是,客户端有可能直接修改超类,从而破坏子类的约束条件。在Properties的情形中,设计者的目标是,只允许字符串作为键(key)和值(value),但是直接访问底层的Hashtable就可以违背这种约束条件。一旦违背了这种约束条件,就不可能再使用Properties API的其它部分(load和store)了。

简而言之,继承的功能非常强大,但是也存在着诸多问题,因为它违背了封装原则。只有当子类和超类之间确实存在子类型关系时,使用继承才是恰当的。即便如此,如果子类和超类处在不同的包中,并且超类并不是为了继承而设计的,那么继承将会导致脆弱性。为了避免这种脆弱性,可以用复合和转发机制来代替继承,尤其是当存在适当的接口可以实现包装类的时候。包装类不仅比子类更加健壮,而且功能也更加强大。

所以,Kotlin中将类设计成final类型时为了提醒开发者慎用继承,如果你考虑清楚确实需要使用继承,那么就给类加open关键字吧!