KotlinAndroid 学习笔记

Kotlin 中类继承的相关概念

2018-05-13  本文已影响1人  mlya

类继承的相关概念

和 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 来修饰.

可见性修饰

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)
}

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

上一篇下一篇

猜你喜欢

热点阅读