美文网首页KotlinAndroid 学习笔记
Kotlin 中类继承的相关概念

Kotlin 中类继承的相关概念

作者: mlya | 来源:发表于2018-05-13 23:24 被阅读1次

    类继承的相关概念

    和 Java 中一样的概念

    接口的定义

    interface Clickable{
        fun click()
    }
    

    和 Java 中不一样的概念

    继承和实现的方式

    1. 用冒号 : 代替了 extendsimplements.
    2. 强制使用关键字 override 来重写方法.
    class Button : Clickable, Focusable {
        override fun click() = println("I was clicked")
    }
    

    接口默认方法

    Java 1.8 支持了接口设置默认方法, Java 中使用 default 关键字来实现默认方法.

    interface Clickable {
        default void click(){
            System.out.println("I'm clickable");
        }
    }
    

    在 Kotlin 的接口中, 我们可以直接设置:

    interface Clickable {
        fun click() = println("I'm clickable!")
    }
    

    和 Java 中相同的是, 如果一个类实现了两个接口, 这两个接口包含有签名相同的默认方法, 那么我们必须显示实现这个方法, 不然会有编译错误.

    Kotlin 和 Java 中对父类方法的调用也有不同:

    interface Clickable {
        fun click()
        // default method in kotlin
        fun showOff() = Println("I'm clickable!")
    }
    
    interface Focusable {
        fun setFocus(b : Boolean) = 
            println("I ${if (b) "got" else "lost"} focus")
    }
    
    class Button : Clickable, Focusable {
        // use override to override a method
        override fun click() = println("I was clicked")
        // cannot use default because the both parents have this method
        override fun showOff() {
            // use cusp brackets to specify parent
            super<Clickable>.showOff()
            super<Focusable>.showOff()
        }
    }
    

    我们都使用了关键字 super 来对父类的调用, 在 Java 中, 我们将基类的名字放在 super 关键字的前面, 比如 Clickable.super.showOff(), 在 Kotlin 中我们将基类放在尖括号中 super<Clickable>.showOff().

    对继承的默认行为 open, final 和 abstract

    在 Java 中, 子类对基类的方法默认是可以重写的 (open 的), 除非使用被显示的标注为了 final. 不恰当的重写可能使子类的实现偏离了基类的设计目的, 这就是所谓的 脆弱的基类 问题, 较好的习惯是要么为继承做好设计并写好文档, 要么就标位 final.

    根据这一思想, 在 Kotlin 中, 默认都是 final 的. 一个类默认是 final 的, 如果你想允许这个类被继承, 那么需要用 open 修饰; 一个方法默认是 final 的, 如果你想允许这个方法被重写, 那么需要用 open 修饰.

    open class RichButton : Clickable {
        // method disable cannot be overwrite by the subclass
        fun disable() {}
        // use open to permit a fun to be overwrite
        open fun animate() {}
        // overwrite a fun, and this fun can be overwrite by subclass. 
        // if you want to prohibit it to be overwrite, use final.
        override fun click() {}
    }
    

    如上例表示的, 如果一个方法是我们从父类继承而来, 那么这个方法默认是可以被重写的, 如果我们不想让其被重写, 那么就需要使用 final 来修饰.

    可见性修饰

    • Java 中的可见性: private, public , protected, package-private.
    • Kotlin 中的可见性: public, private, protected, internal.

    internal: 模块内可见, 一个模块就是一组一起编译的 Kotlin 文件, 可能是一个 Intellij IDEA 模块, 一个 Eclipse 项目, 一个 Maven 或 Gradle 项目或者一组使用调用 Ant 任务进行编译的文件.

    Java 中 protected 成员可以被该类和其子类和同一个包内访问, 而 Kotlin 中只能被该类和子类中可见.

    同样要注意的是, Kotlin 的扩展函数不能访问 private 和 protected 成员.

    内部类和嵌套类

    什么是内部类和嵌套类:

    Java 有内部类的概念, 内部类会隐式地存储它的外部类引用, 这在有些情况下会出现问题.

    Java 中如果想声明一个类为嵌套类, 使用 static 关键字.

    Kotlin 的默认行为不同, Kotlin 默认为嵌套类, 如果声明内部类, 使用 inner 关键字.

    Kotlin 访问外部类使用 this@Outer 的方式. 而 Java 中使用 Outer.this 的方式.

    class Outer {
        inner class Inner {
            fun getOuterReference(): Outer = this@Outer
        }
    }
    

    密封类

    Kotlin 增加了一种定义受限的继承结构: 密封类

    密封类使用 sealed 关键字修饰, 默认为 open 的. sealed 类对子类做出了限制, 要求子类必须定义在同一个文件中, 编译时会进行检查, 适用于如下场景:

    sealed class Expr {
        class Num(val value: Int) : Expr()
        class Sum(val left: Expr, val right: Expr) : Expr()
    }
    
    class Sub(val left: Expr, val right: Expr) : Expr()
    
    fun eval(e: Expr): Int = when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.left) + eval(e.right)
        is Sub -> eval(e.left) - eval(e.right)
    }
    
    fun main(args: Array<String>) {
        val no1 = Expr.Num(3)
        val no2 = Expr.Num(1)
        val sub = Sub(no1, no2)
        println(eval(sub))
    }
    

    我们在 when 中需要判断具体是什么类型, 如果我们添加默认类型, 那么可能会因为我们添加了新的子类而没有修改 when 而产生 bug (因为新的子类的情况会导向默认情况), 而 sealed 类将限制子类, 编译器将知道有哪些子类, 所以我们不用添加默认情况 (else), 在有了新的子类后, 将会检查我们需不需要添加新的分支.

    类初始化

    Java 中类的初始化我们都非常熟悉了, 通过构造方法来实现, 可以声明一个或多个构造方法.

    Kotlin 中, 对主构造方法和从构造方法作了区分

    主构造方法

    主构造方法一般是主要而间接的初始化类的方法, 比如下面的简单的类.

    class User(val nickname: String)
    

    主构造方法就是括号中的内容 (对, 就这么简单), 它完成了两个功能, 一个是声明了 nickname 这个属性, 再一个是完成了这个属性的初始化.

    对的, 主构造方法没有语句块, 如果需要一些初始化语句, 需要在类内部使用 init 初始化语句块:

    class User(val nickname: String) {
        
        init {
            // init 语句块, 完成一些初始化工作.
            // 在类创建时执行
            // 可以创建多个 init 语句块
        }
    }
    

    另一个需求是对主构造方法由访问限制, 比如如果我们想要一个 private 的主构造方法, 那么使用如下形式:

    class User private constructor(val nickname: String)
    

    这里使用了 constructor 关键字, 用 private 进行修饰, 表示这是一个 private 的主构造方法.

    继承

    继承中, 子类的构造方法要对父类进行初始化, 用如下形式:

    open class User(val nickname: String) { ... }
    
    class MyUser(nickname: String) : User(nickname) { ... }
    

    注意冒号后面的部分, 继承一个类, 必须显示的调用其主构造方法进行初始化 (即使父类没有任何参数也要显示调用).

    继承一个接口, 接口没有构造方法, 所以, 实现一个借口时, 是没有后面那么括号的.

    // 即使父类构造构造方法没有参数也要显式调用
    class RadioButton : Button()
    
    // 接口没有构造方法
    interface SimpleInterface
    class MyClass : SimpleInterface
    

    从构造方法

    首先需要注意, 多个构造方法在 Kotlin 中不如 Java 常见, 因为 Kotlin 支持参数默认值和参数命名的语法.

    但是还是会有需要多个构造方法的场景, 使用从构造方法:

    open class view {
        constructor(ctx: Context) {
            // some code
        }
        
        constructor(ctx: Context, attr: AttributeSet) {
            // some code
        }
    }
    

    将一个构造方法委托给另一个构造方法 (从一个构造方法中调用自己的类的另一个构造方法), 使用 this() 关键字:

    open class Bus {
        val name: String
        val price: Int
        constructor(name: String): this(name, 10) {
            println("一辆新车")
        }
    
        constructor(name: String, price: Int){
            this.name = name
            this.price = price
            println("name: $name\nprice: $price")
        }
    }
    

    当我们使用第一个构造方法时, 会先调用第二个构造方法, 再调用自己的语句块.

    这里这个例子是为了说明从构造方法, 实际上我们可以使用默认值来代替, 更加简洁:

    open class Bus(val name: String, val price: Int = 10)
    

    继承

    class MyBus: Bus {
        constructor(name: String): this(name, 10)
        
        constructor(name: String, price: Int): super(name, price)
    }
    

    实现接口中声明的属性

    在 Kotlin 中, 接口可以包含抽象属性声明, 例如:

    interface User {
        val nickname: String
    }
    

    这表示, 实现 User 接口的类, 需要提供一个取得 nickname 的值的方式, 获取值的方法主要有三种: 主构造方法, 自定义 getter(), 属性初始化.

    // 主构造方法属性
    class PrivateUser(override val nickname: String) : User
    
    // 自定义 getter()
    class SubscribingUser(val email: String) : User {
        override val nickname: String
            get() = email.subStringBefore('@')
    }
        
    // 属性初始化
    class FacebookUser(val accountId: Int) : User {
        override val nickname = getFacebookName(accountId)
    }
    

    我们这里思考一下第二种和第三种初始化方式有什么区别?

    • 第二种, 自定义 getter() 的初始化方法, 我们并没有一个字段来存储 nickname 这个变量, 而是在需要获取该值的时候, 从 email 中截取, 截取的开销很小, 所以只需在需要的时候获取.
    • 第三种, 我们存在一个字段来存储 nickname, 因为 getFacebookName() 方法是一个开销很大的方法, 可能需要网络请求或者数据库查询, 合理的方法是在初始化的时候获取一次并存储在一个字段中.

    相关文章

      网友评论

        本文标题:Kotlin 中类继承的相关概念

        本文链接:https://www.haomeiwen.com/subject/kdvrdftx.html