《Kotlin核心编程》 ——3.2 不同的访问控制原则

举报
华章计算机 发表于 2020/02/21 22:37:14 2020/02/21
【摘要】 本节书摘来自华章计算机《Kotlin核心编程》 —— 书中第3章,第3.2.1节,作者是水滴技术团队 。

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的限制修饰符比较

image.png


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。