Kotlin学习(七): 类和继承、接口与实现

Safe

本文是学习Kotlin的类和继承相关,嵌套类相关,接口相关。

Kotlin的类的声明与Java一样,使用class关键字

class Invoice {
}

类的声明包含类名,类头(指定类型参数,主构造函数等等),以及类主体,用大括号包裹。类头和类体是可选的;如果没有类体可以省略大括号。

class Empty

构造函数

Kotlin的类有两种构造函数,一种是主构造函数,一种是二级构造函数。
主构造函数是写在类名后面(可以有可选的类型参数),使用constructor关键字声明

class Person constructor(firstName: String) {
}

如果一个非抽象类没有声明构造函数(主构造函数或二级构造函数),它会产生一个没有参数的构造函数。构造函数是 public 。如果你不想类有公共的构造函数,就得声明一个空的主构造函数

class DontCreateMe private constructor () {
}

如果主构造函数没有任何注释或可见性修饰符,则可以省略constructor关键字

class Person(firstName: String) {
}

主构造函数不包含任何代码,初始化代码应该放在以init做前缀的初始化块中

class Customer(name: String) {
    init {
        logger.info("Customer initialized with value ${name}")
    }
}

主构造函数的参数既可以用在初始化块中,也可以用在类的属性初始化声明处

class Customer(name: String) {
    val customerKry = name.toUpperCase()
}

一般用Java写构造函数的时候,里面是写属性的初始化,如下面

class Person {
    private String firstName;
    private String lastName;
    private int age;
    Person(String firstName, String lastName, int age) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
}

Kotlin有更简洁的写法,可以在主构造函数重声明属性并在主构造函数中初始化,同时可以设置属性为可变(var)或者设置为只读(val

class Person(val firstName: String, val lastName: String, var age: Int){
}

如果构造函数具有注解(@Inject)或可见性修饰符(public),则constructor关键字必须写上,并且写在可见性和注解的后面

class Customer public @Inject constructor(name: String) {  }

二级构造函数

二级构造函数同样是以constructor关键字来声明,与主构造函数不同的是,二级构造函数是写在类里面,同时,二级构造函数可以有多个。

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }

    constructor(parent: Person, count: Int) {
        parent.children.add(this)
    }
}

如果该类有主构造函数,那么声明二级构造函数都要使用this关键字来代理(Delegation)主构造函数

class Person(val name: String) {
    constructor(name: String, paret: Person) : this(name) {
        parent.children.add(this)
    }
    constructor(name: String, parent: Person, count: Int) : this(name) {
        parent.children.add(this)
    }
}

注意:在 JVM 虚拟机中,如果主构造函数的所有参数都有默认值,编译器会生成一个附加的无参的构造函数,这个构造函数会直接使用默认值。

class Customer(val customerName: String = "")

创建类的实例

Kotlin创建类的实例不需要像Java一样使用到new关键字,直接调用构造函数即可创建类的实例

val invoice = Invoice()

val customer = Customer("Joe Smith")

类成员

Kotlin的类里面可以包含

  • 构造函数和初始化块(Constructors and initializer blocks)
  • 函数(Functions)
  • 属性(Properties)
  • 嵌套类和内部类(Nested and Inner Classes)
  • 数据对象(Object Declarations)

继承

Kotlin中的所有类都有一个公共的父类Any

class Example // 隐式继承自Any

Any不是java.lang.ObjectAny只包含equals(),hashCode()以及toString()三个成员

Any
Object

如何继承一个类,声明一个类的父类呢,在类后面加冒号然后再加父类

open class Base(p: Int)

class Derived(p: Int) : Base(p)

注意一个类如果要被别的类继承,要加上open关键字,在类名的最前面,默认情况下,Kotlin中的所有类都是final的,Kotlin的接口以及成员默认都是open

同样,如果父类有主构造函数,则子类必须在主构造函数中初始化

/**
* 父类
*/
open class BaseKot(name: String) {
}

/**
* 子类
*/
class Kot(name: String) : BaseKot(name) {
}

如果子类没有主构造函数,则可以在二级构造函数中使用super关键字初始化或者在代理(Delegation)另一个构造函数。

/**
* 父类
*/
open class BaseKot(name: String) {
}

/**
* 子类
*/
class Kot : BaseKot {
    constructor(name: String) : super(name) {

    }
}

下面这种情形中不同的二级构造函数可以调用父类不同的构造方法

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx,attrs) {
    }
}

重写方法

上面说了,如果一个类要被继承需要在父类的类名前面加上open关键字,该类才能被继承,同样,一个父类的方法如果要能被重写,也需要加上open关键字,如果没有加的话,子类是不能重写改方法的。

/**
* 父类
*/
open class BaseKot {
    open fun v() {}
    fun nv() {}
}

/**
* 子类
*/
class Kot : BaseKot() {
    // 加final可以设置不被重写
    override fun v() {
        super.v()
    }
    // 重写nv方法会报错,加override也不行,只有父类的nv方法设置为open才行
    // fun nv() { }
}

同样,如果类不是open的话,里面的成员也不允许设置为open

重写属性

重写属性与重写方法类似,并且属性必须具有兼容类型,可以将val属性重写成var,也可以将var属性重写成val

/**
* 父类
*/
open class BaseKot {
    open val age: Int get() = 0 // get() = 0 与 = 0相同
    open fun v() {}
}

/**
* 子类
*/
class Kot : BaseKot() {
    override var age: Int = 5 // 重写为var类型

    override fun v() {
        super.v()
    }
}

可以在子类的主构造函数中声明override

class Kot(override var age: Int) : BaseKot() {

    override fun v() {
        super.v()
    }
}

使用super实现

子类中的代码可以使用super关键字调用其父类的函数与属性访问器的实现:

open class Foo {
    open fun f() { println("Foo.f()") }
    open val x: Int get() = 1
}

class Bar : Foo() {
    override fun f() { 
        super.f()
        println("Bar.f()") 
    }
    
    override val x: Int get() = super.x + 1
}

在一个内部类中访问外部类的父类,可以通过由外部类名限定的super关键字来实现:super@Outer

class Bar : Foo() {
    override fun f() { /* …… */ }
    override val x: Int get() = 0
    
    inner class Baz {
        fun g() {
            super@Bar.f() // 调用 Foo 实现的 f()
            println(super@Bar.x) // 使用 Foo 实现的 x 的 getter
        }
    }
}

重写规则

在Kotlin中,实现继承通常遵循如下规则:如果一个类从它的直接父类继承了同一个成员的多个实现,那么它必须复写这个成员并且提供自己的实现,要使用父类中提供的方法,用super<Base>来表示

open class A {
    open fun f() { print("A") }
    fun a() { print("a") } // 不会被重写
}

interface B {
    fun f() { print("B") } // 接口成员默认是open的
    fun b() { print("b") }
}

class C() : A(), B {
    // The compiler requires f() to be overridden:
    override fun f() {
        super<A>.f() // call to A.f()
        super<B>.f() // call to B.f()
    }
}

可以同时从A B中继承方法,而且C继承a()b()的实现没有任何问题,因为它们都只有一个实现。但是f()有两个实现,所以要在C中必须复写f()并且提供自己的实现。

抽象类

与Java类似,Kotlin的类或者成员都可以被声明为abstract,同时不需要添加open关键字。
可以用一个抽象成员去复写一个带 open 注解的非抽象方法

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

枚举类

Kotlin也支持枚举,每个枚举常量都是一个对象。

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

初始化

由于每个枚举都是枚举类的实例,因此可以进行初始化

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

匿名类

枚举常量也可以使用对应的方法声明自己的匿名类,以及覆盖基本方法。

注意,与Java一样,如果枚举类定义了任何成员,则需要使用分号将成员定义中的枚举常量定义分隔开。

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };

    abstract fun signal(): ProtocolState
}

使用枚举常量

就像在Java中一样,Kotlin中的枚举类有合成方法,允许列出定义的枚举常量,并通过名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是EnumClass):

EnumClass.valueOf(value: String): EnumClass

EnumClass.values(): Array<EnumClass>

如果指定的名称与类中定义的任何枚举常量不匹配,该valueOf()方法将抛出IllegalArgumentException

每个枚举常量都具有在枚举类声明中获取其名称和位置的属性:

val name: String

val ordinal: Int

密封类

密封类是用来表示受限的类层次结构。例如当一个值为有限集中的 类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。
密封类是类名前加sealed关键字来声明

sealed class Expr {
    class Const(val number: Double) : Expr()
    class Sum(val e1: Expr, val e2: Expr) : Expr()
    object NotANumber : Expr()
}

密封类子类的扩展可以在任何地方,不必在密封类声明内部进行

使用密封类的最主要的的好处体现在使用when表达式。可以确保声明可以覆盖到所有的情形,不需要再使用 else情形。

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
    // 前面说过,使用when的时候除非能把所有可能的情况都列举出来,不然一定要加上else
    // 这里就不需要,因为列举了所有的情况
}

嵌套类

与Java一样,一个类中可以嵌套其他的类

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

如何调用

val demo = Outer.Nested().foo() //==2

内部类

内部类是使用inner关键字来声明。

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

如何调用

val demo = Outer().Inner().foo() // == 1

嵌套类与内部类的区别是:嵌套类不能够访问外部成员,因为内部类包含对外部类的对象的引用,所以能够访问外部类的成员;调用的时候,内部类需要使用外部类的实例来调用。

接口

Kotlin的接口类似与Java 8,都可以包含抽象方法,以及方法的实现。和抽象类不同的是,接口不能保存状态。可以有属性但必须是抽象的,或者提供访问器(getter)的实现。
接口的定义同样是使用interface关键字

interface MyInterface {
    fun bar()
    fun foo() {
      // 函数体是可选的,自己实现方法
    }
}

实现接口

一个类或对象可以实现一个或多个接口

class Child : MyInterface, YouInterface {
    override fun bar() {
        // body
    }
}

接口中的属性

interface MyInterface {
    val prop: Int // abstract

    val propertyWithImplementation: String
        get() = "foo" // 提供访问器getter,可以直接获取propertyWithImplementation的值foo

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
    override val propertyWithImplementation: String = "123"
}

解决重写冲突

同类的重写类似,接口也一样,如果父类中声明了许多类型,有可能出现一个方法的多种实现,必须复写这个成员并且提供自己的实现,要使用父类中提供的方法,用super<Base>来表示

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }
}

上面的代码是AB接口都有声明了foo()bar()函数,都实现了foo()方法,但只有B实现了bar()bar()A中并没有声明它是抽象的,C实体类需要重写并实现bar(),而D类,不用重写bar()方法,因为继承的B接口已经实现了。由于继承了两个foo()的实现,所以用super<Base>来区分。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容