《Kotlin核心编程》 ——3.2 不同的访问控制原则
3.2 不同的访问控制原则
在构造完一个类的对象之后,你需要开始思考它的访问控制了。在Java中,如果我们不希望一个类被别人继承或修改,那么就可以用final来修饰它。同时,我们还可以用public、private、protected等修饰符来描述一个类、方法或属性的可见性。对于Java的这些修饰符,你可能已经非常熟悉,其实在Kotlin中与其大同小异。最大的不同是,Kotlin在默认修饰符的设计上采用了与Java不同的思路。通过本节的内容你会发现,Kotlin相比Java,对一个类、方法或属性有着不一样的访问控制原则。
3.2.1 限制修饰符
当你想要指定一个类、方法或属性的修改或者重写权限时,你就需要用到限制修饰符。我们知道,继承是面向对象的基本特征之一,继承虽然灵活,但如果被滥用就会引起一些问题。还是拿之前的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的设计真的就那么好吗?
2.类默认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的限制修饰符比较
- 点赞
- 收藏
- 关注作者
评论(0)