Kotlin基础知识三——接口与类

274 阅读8分钟

1.接口

(1)interface关键字

使用interface关键字而不是class来声明一个Kotlin的接口

//声明接口
interface Clickable {
    fun click()
}

//实现接口
class Button: Clickable {
    override fun click() = println("I was clicked")
}

Kotlin在类名后面使用冒号来替代Java中的extendsimplements关键字。 和Java一样,一个类可以实现任何多个接口,但是只能继承一个类

区别:

  • 与Java中的@Override注解类似,override修饰符用来标注被重写的父类或者接口的方法和属性。

  • Kotlin中使用override修饰符是强制要求的,这会避免先写出实现方法再添加抽象方法造成的意外重写。

(2)Kotlin方法默认实现

接口的方法可以有一个默认实现。 与Java 8不同的是,Java 8中需要你在这样的实现上标注default关键字,对于这样的方法,Kotlin没有特殊的注解:只需要提供一个方法体。

interface Clickable {
    fun click()
    //带默认实现的方法
    fun showOff() = println("I'm clickable!")
}

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if(b) "got" else "lost"} focus.")
    
    fun showOff() = println("I'm focusable!")
}

实现这个接口,需为click提供一个实现。可以重新定义showOff方法的行为,也可直接使用默认行为而直接忽略。

若同一个类继承两个接口,并且这两个接口都包含带默认实现的方法,则该类无法使用这两个接口中的带默认实现的方法,需自行实现,否则会报错

子类调用父类重写方法的格式如下: super<FatherClass>.function()

示例:

class Button: Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

在Java中实现包含方法体的接口

Kotlin 1.0是以Java 6为目标设计的,其并不支持接口中的默认方法。因此它会把每个带默认方法的接口编译成一个普通接口和一个将方法体作为静态函数的类的结合体。

2.open、final和abstract修饰符:默认为final

(1)open关键字

Java的类和方法默认是open的,即可重写

Kotlin的类和方法默认是final的,即不能重写

// 这个类是open的:其他类可以继承它
open class RichButton: Clickable {
    // 这个函数是final的:不能再子类中重写它
    fun disable() {}
    
    // 这个函数是open的:可以在子类中重写它
    open fun animate() {}
    
    // 这个函数重写了一个open函数并且他本身同样是open的
    override fun click() {}
    
    /*
    // 在这里“final”并没有被删减是因为没有"final"的
    // “override”意味着是open的
    final override fun click() {}
    */
}

注意:重写一个基类或者接口的成员,重写了的成员默认是open的。

若想改变这一行为,阻止该类的子类重写,可以显示地将重写的成员标注为final

(2)abstract关键字

一个类声明为abstract时,该类不能被实体化。一个抽象类通常包含一些没有实现并且必须在子类重写抽象成员

抽象成员始终是open的,所以不需要显示地使用open修饰符。

// 这个类是抽象的:不能创建它的实例
abstact class Aniamted {
    // 这个函数是抽象的:它没有实现必须被子类重写
    abstract fun animate()
    
    // 抽象类中的非抽象方法函数并不是默认open的,
    // 但是可以标注为open的
    open fun stopAnimating() {}
    fun animateTwice()
}

总结

在接口中,不能使用final、open或abstract。接口中的成员始终是open的,不能将其声明为final的。

图 1 类中访问修饰符的意义
修饰符 相关成员 评注
final 不能被重写 类中成员默认使用
open 可以被重写 需要明确地表明
abstract 必须被重写 只能在抽象类中使用;抽象成员不能有实现
override 重写父类或接口的成员 如果没有使用final表面,重写的成员默认是开放的

2.可见性修饰符

可见性修饰符帮助控制对代码库中声明的访问。通过限制类中的实现细节的可见性,可以确保在修改它们时避免破坏依赖这个类的代码的风险。

Java中默认可见性——包私有(default),在Kotlin中并没有使用。Kotlin只把包作为在命名空间里组织代码的一种方式使用,并没有将其作为可见性控件。

internal,表示“只在模块内部可见

一个模块就是一组一起编译懂得Kotlin文件。这有可能是一个Intelij IDEA模块、一个Eclipse项目等等。

internal可见性的优势:

  • 提供了对模块实现细节的真正封装。而使用Java时,这种封装很容易被破坏,因为外部代码可以将类定义到与你代码相同的包中,从而得到访问你的包私有声明的权限。
  • Kotlin允许在顶层声明中使用private可见性,包括类、函数和属性。这些声明都会只在声明它们的文件中可见。
图 2 Kotlin的可见性修饰符
修饰符 类成员 顶层声明
public(默认) 所以地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见 ——
private 类中可见 文件中可见

示例:

internal open class TalkativeButton: Focusable {
    private fun yell() = println("Hey!")

    protected fun whisper() = println("Let's talk!")
}

internal fun TalkativeButton.giveSpeech() =
        println("Speech!")

一个通用的规则是:类的基础类型和类型参数列表中用到的所有类,或者函数的签名都有与这个类或者函数本身相同的可见性

这个规则可以确保你在需要调用函数或者继承一个类时能够始终访问到所有的类型。

Kotlin的可见性修饰符和Java

Kotlin中的public、protected和private修饰符在编译成Java字节码时会被保留。你从Java代码使用这些Kotlin声明时就如同他们在Java中声明了同样的可见性。唯一的例外是private类:在这种情况下它会被编译成包私有声明(在Java中你不能把类声明为private)。

internal:Java中没有直接与之类似的东西。包私有可见性是一个完成不同的东西:一个模块通常会由多个包组成,并且不同模块可能会包含来自同一个包的声明。因此internal修饰符在字节码中会变成public。

3.内部类和嵌套类:默认是嵌套类

在Kotlin中可以在另一个类中声明一个类。这样做在封装一个辅助类或者把一些代码放到靠近它被使用的地方时非常有用。

区别在于:Kotlin的嵌套类不能访问外部类的实例

Kotlin中没有显式修饰符的嵌套类与Java中的static嵌套类时一样的。要把他变成一个内部类来持有一个外部类的引用的话,需要使用inner修饰符

图 3 嵌套类和内部类在Java与Kotlin中的对应关系
类A在另一个类B中声明 在Java中 在Kotlin中
嵌套类(不存储外部类的引用) static class A class A
内部类(存储外部类的引用) class A inner class A

在Kotlin中引用外部类实例的语法也与Java不同。需要使用this@OuterInner类去访问Outer类

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

3.密封类:定义受限的类继承结构

sealed类:为父类添加一个sealed修饰符,对可能创建的子类做出严格的限制。所有的直接子类必须嵌套在父类中

  • 密封类示例
// 将基类标记为密封的
sealed class Expr {
    //将所有可能的类作为嵌套类列出
    class Num(val value: Int): Expr()
    class Sum(val left: Expr, val right: Expr): Expr()
}

fun eval(e: Expr): Int = 
    when(e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • 非密封类示例
class Expr {
    class Num(val value: Int): Expr()
    class Sum(val left: Expr, val right: Expr): Expr()
}

fun eval(e: Expr): Int = 
    when(e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
        else -> 
            throw IllegalArgumentException("Unknown expression")
    }

如果再when表达式中处理所有sealed类的子类,你就不再需要提供默认分支。

sealed修饰符隐含的这个类是一个open类,你不再需要显示地添加open修饰符。

when中使用sealed类并且添加一个新的子类的时候,有返回值的when表达式会导致编译失败。

sealed class Expr {
    //将所有可能的类作为嵌套类列出
    class Num(val value: Int): Expr()
    class Sum(val left: Expr, val right: Expr): Expr()
    
    //额外添加的子类
    class divide(val value: Int): Expr()
}

fun eval(e: Expr): Int =
        when(e) {
            is Expr.Num -> e.value
            is Expr.Sum -> eval(e.right) + eval(e.left)
            // 这儿没添加对应的判断
        }

就会报错:

即:when表达式必须面面俱到,需要添加divide分支else分支

这种情况下,Expr类有一个只能在类内部调用的private构造方法。你也不能声明一个sealed接口。 因为这样做,Kot编译器不能保证任何人都不能再Java代码中实现额这个接口。