Kotlin官方参考整理——03.类和对象1

Kotlin中没有static关键字。如果你需要像Java中的静态函数那样方便调用的函数,Kolin官方推荐使用定义在顶层位置的函数。

3 类和对象

3.1 定义类

Kotlin中使用关键字class声明类,如class Invoice

类声明由类名、类头(类头包括类型参数(即泛型)、主构造函数等)和类体(即{…}部分)构成。类头和类体都是可选的。

主构造函数

一个类可以有一个主构造函数和多个次构造函数。

主构造函数是类头的一部分,它跟在类名和可选的类型参数之后:

class Person constructor(firstName: String) {
}

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字:

class Person(firstName: String) {
}

如果主构造函数有注解或可见性修饰符,则不能省略constructor关键字:

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

主构造函数不能包含任何的代码。初始化工作可以放到以init开头的初始化块(initializer blocks)中进行:

class Customer(name: String) {
    init {
        初始化工作...
        //在初始化块中可以使用主构造函数的参数
    }
}

在类的属性的初始化器(initializer)中也可以使用主构造函数的参数:

class Customer(name: String) {
    val customerKey = name  //这就是所谓的“初始化器”
}

//对比:在java中只能这样
class Customer{
    String customerKey;

    Customer(String name){
        this.customerKey = name;
    }
}

☆实际上,声明属性并用主构造函数的参数来初始化属性有更简单的写法(常用):

class Customer(val customerKey: String) {   
}

与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。

次构造函数

也可以通过关键字constructor来声明次构造函数,次构造函数的参数之前不允许有var或val:

class Person {
    constructor(parent: Person) {
        ...
    }
}

如果类有主构造函数,则每个次构造函数必须委托到主构造函数(即调用主构造函数),可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数使用this关键字即可:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        ...
    }
}

如果一个类没有声明任何主或次构造函数,则会自动生成一个无参数的主构造函数,此构造函数的默认可见性为public。如果不希望在外部创建此类的对象(比如写单列模式的时候),则随便声明一个private的构造函数即可,一般是声明一个空参数的主构造函数:

class DontCreateMe private constructor () {
}

抽象类

类中的成员都可以声明为抽象(abstract)的,抽象的成员函数没有函数体,抽象的成员变量不能进行初始化(在《01开始.md》中的“1.1.4 定义变量”中提到过这一点)。含有抽象成员的类也是抽象的,抽象类不能创建对象。

注意,尽管抽象类不能创建对象,但抽象类是有构造函数的。

3.2 创建对象

Kotlin中没有new关键字。创建对象的格式非常简单,就像调用函数一样:

val invoice = Invoice()
val customer = Customer("Joe Smith")

3.3 类的成员

类可以包含:

  • 构造函数和初始化块
  • 方法(即成员函数)
  • 属性(即成员变量)
  • 嵌套类和内部类
  • 对象声明

3.4 继承

3.4.1 继承的格式

默认情况下,Kotlin中所有的类都是final的(抽象类默认是open的),即不允许被继承,若要让其可以被继承,需要将其声明为“open”。

在Kotlin中所有类都有一个共同的超类Any,类似于Java中类Object的地位。

要让一个类继承类A,只要在类名后跟上“:A”即可:

(1) 如果派生类有主构造函数,则必须就地调用基类的构造函数(基类名后直接跟(…)):

open class Base //没有定义任何构造函数,则会自动生成一个空参数的主构造函数

//派生类有主构造函数,则必须就地调用基类的构造函数(基类名后直接跟(...))
class Derived1(val name: String) : Base() {
    fun printName() {
        print(name)
    }
}

(2) 如果派生类没有主构造函数,则必须在所有次构造函数后面调用基类的构造函数(使用super(…)),或委托给另一个构造函数做到这一点:

//派生类没有主构造函数,则必须在所有次构造函数后面调用基类的构造函数(使用super(...))
class Derived2:Base{
    constructor():super()
    constructor(str:String):super()
    constructor(i:Int):this()
}

3.4.2 覆写方法和属性

Java中只有方法有覆写的概念,而Kotlin中方法和属性都是可以覆写的。默认情况下,Kotlin类中的方法和属性都是final的(抽象的方法和属性默认是open的),即不允许被子类覆写,要让其可以被子类覆写,需要显式的将其声明为open。而子类中覆写的方法和属性必须显式的声明为override。

open class Base {
    open fun v() {}     //允许覆写
    fun nv() {}         //不允许覆写
}

class Derived : Base() {
    override fun v() {} //覆写父类的v()方法
}

标记为override的成员本身是开放的,也就是说,它可以在子类中被覆写。如果你想禁止再次覆写,使用final关键字:

open class AnotherDerived : Base() {
    final override fun v() {}
}

对于属性,var可以覆写val,但val不能覆写var:

open class Father {
    open val x: Int = 1
}

class Son : Father() {
    override var x: Int = 2
}

可以在主构造函数中使用override关键字作为属性声明的一部分,如class Son(override val count: Int) : Father(){...}

3.5 属性

当我们定义了一个顶层变量或成员变量,系统会自动为其生成访问器(getter、setter)。

带有访问器的变量称为属性。顶层变量和成员变量都有访问器,而局部变量没有,因此顶层变量和成员变量都是属性,而局部变量不是。

当我们定义了一个属性(即一个顶层变量或成员变量),我们所看到的变量其实只是一个名字而已,本质上它有两个组成部分:访问器(getter、setter)和幕后字段。幕后字段不是必须的。

可以这样理解:getter和setter才是属性的本体,因为属性可以没有幕后字段(后面会提到)。

对比Java:在Java中,属性 = 成员变量 = 字段。

定义和使用属性的一般格式:

class Address {
    var name: String = ……
    var street: String = ……
    var city: String = ……
    var state: String? = ……
}
fun copyAddress(address: Address): Address {
    val result = Address()

    result.name = address.name
    result.street = address.street
    ...

    return result
}

定义属性的完整格式:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

val <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]  

方括号中的部分都不是必须的。在上面Address类的例子中,系统为name、street等属性生成了隐含的默认getter和setter。

也可以自己定义setter和getter:

class Person {
    var name: String = "Lisi"
        get() {
            return field //field的含义后面会说
        }
        set(value) {
            field = value
        }
}

我们对属性的任何访问操作,都是通过其getter和setter来完成的:当我们读取属性的值时,会调用其getter,当我们向属性写入值时,会调用其setter:

val person = Person()
val name = person.name  //会通过name属性的getter来读取属性值
person.name = "LiSi"    //会通过name属性的setter来写入值

幕后字段(backing field)

我们希望对name属性的getter和setter动一点手脚,比如像下面这样:

class Person {
    var name: String = "ZhangSan"
        get() {
            return name + "哈哈哈"
        }
        set(value) {
            name = value + "呵呵呵"
        }
}

当用户读取name时,我们返回给访问者的并不是name的真实值,而是加了”哈哈哈”后缀;当用户往name写入值时,我们写入的也不是用户给的值,而是在用户给的值后拼接了”呵呵呵”。看起来只是一个简单的恶作剧,似乎没什么的大问题。

然后当程序运行起来之后,你会发现,对name的任何读取操作都会导致getter的无限递归调用,最终引发栈溢出和程序崩溃;对name的任何写入操作都会导致setter的无限递归调用,最终引发栈溢出和程序崩溃。为什么会这样呢?答案显而易见,上面已经说过了:对属性的任何读取操作都会调用其getter,对属性的任何写入操作都会调用其setter,在getter和setter内部读写属性也不例外。

那么在getter和setter中该如何读写属性(而不引起getter和setter的递归调用)呢?答案就是使用幕后字段(backing field)。在getter和setter中使用“field”来表示幕后字段。那么上面的那个恶作剧就应该写成这样:

class Person {
    var name: String = "ZhangSan"
        get() {
            return field + "哈哈哈"
        }
        set(value) {
            field = value + "呵呵呵"
        }
}

幕后字段是真正存储属性的值的地方。之所以叫做“幕后”,是因为该字段是隐藏在属性内部或者说是“背后”的,属性的使用者只知道自己在读写属性,并不会感知到该字段的存在,因此叫做“幕后字段”。

一个属性也可以没有幕后字段。当我们显式定义了getter和setter,并且在getter和setter中都没有用到幕后字段时,则这个属性就是没有幕后字段的。不能对没有幕后字段的属性进行初始化(因为没有存储值的地方了):

class Person {
    //没有幕后字段,因此不能对name进行初始化
    var name: String // = "ZhangSan"
        get() {
            return "haha"
        }
        set(value) {
            Log.e("Log","do nothing")
        }
}

3.6 接口

接口中的属性和方法默认都是抽象的,可以省略abstract关键字。

与Java不同的是,Kotlin的接口中还允许存在已实现的方法(即非抽象的方法)。Kotin的接口与抽象类的区别在于,接口中只允许存在抽象属性:

interface C{
    var a:Int//抽象属性
    var b:Int = 1//报错,不允许有非抽象的属性

    fun aaa()//抽象方法

    fun bbb(){//可以有非抽象的方法
    }
}

与类和抽象类不同,接口中没有也不能有构造函数,因此,当子类实现接口时自然也就不需要去调用接口的构造函数了:

interface MyInterface {
    fun bar()
    fun foo() {
        ...
    }
}

class Child : MyInterface {//实现接口
    override fun bar() {
        ...
    }
}

单继承、多实现以及避免歧义

和Java一样,Kotlin中的类也只能继承一个父类,但是可以实现多个接口,即“单继承、多实现”。

open class A {
    open fun f() {
        print("A")
    }

    fun a() {
        print("a")
    }
}

interface B {
    fun f() {
        print("B")
    }

    fun b() {
        print("b")
    }
}

interface C {
    fun f() {
        print("C")
    }

    fun c() {
        print("c")
    }
}



class Z: A(),B,C {
    //☆注意:为了避免歧义,编译器要求必须覆写f()
    override fun f() {
        super<A>.f()    // 调用 A.f()
        super<B>.f()    // 调用 B.f()
        super<C>.f()    // 调用 C.f()
    }
}

3.7 扩展类

Kotlin能够为一个类扩展新功能而无需继承该类或使用装饰者模式。Kotlin支持扩展函数和扩展属性,即动态的为一个类添加函数或属性。

扩展不能真正的修改他们所扩展的类。通过定义一个扩展,你并没有在一个类中插入新成员,而仅仅是可以通过该类型的对象去访问扩展的函数或属性而已。

3.7.1 扩展函数

声明一个扩展函数,我们只需以接收者类型(也就是被扩展的类型)作为函数的前缀即可。例如,下面代码为MutableList添加了一个swap函数:

//声明扩展函数
fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] //this指接收者对象,即MutableList<Int>对象
    this[index1] = this[index2]
    this[index2] = tmp
}

//然后就可以在MutableList<Int>对象上调用swap函数了:
val list = mutableListOf(1, 2, 3)
list.swap(0, 2)

我们还可以声明一个泛化的扩展函数(参考函数泛型相关章节):

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1]
    this[index1] = this[index2]
    this[index2] = tmp
}

扩展是静态解析的

调用的扩展函数是由函数调用所在的表达式的类型来决定的,而不是由表达式在运行时的求值结果决定的。例如:

open class C
class D: C()

fun C.foo() = "c"
fun D.foo() = "d"

fun printFoo(c: C) {
    println(c.foo())
}

//实际输出的是c而不是d。因为调用的扩展函数只取决于参数c的声明类型,该类型是C。
printFoo(D())   

可空接收者

可以为可空的接收者类型定义扩展函数。可以在扩展函数体内检测接收者是否为null:

fun Any?.toString(): String {
    if (this == null) return "null"
    return toString()
}

3.7.2 扩展属性

声明一个扩展属性,我们只需以接收者类型(也就是被扩展的类型)作为属性的前缀即可。

val Man.gender:String
    get() = "male"  

需要注意的是,扩展属性都是没有幕后字段的,因此不能进行初始化,例如val Man.gender = "male"就是错误的。扩展属性的行为只能由显式提供的getters/setters定义。

3.7.3 声明扩展的位置

在顶层位置声明扩展

大多数时候我们都在顶层定义扩展,即直接在包里:

package foo.bar

//为Baz类添加一个goo()函数
fun Baz.goo() {
    ...
}

如果要在另一个包的文件中使用扩展,我们需要先导入它:

package com.example.usage

import foo.bar.goo
//或者import foo.bar.*

fun usage(baz: Baz) {
    baz.goo()
)

在类中声明扩展

在一个类内部你可以为另一个类声明扩展。扩展声明所在的类的实例称为分发接收者,被扩展的类的实例称为扩展接收者。在扩展函数内部可以直接访问分发接收者和扩展接收者的成员:

class D {
    fun bar() { …… }
} 
class C {
    fun baz() { …… }

    fun D.foo() {
        bar() //调用D.bar
        baz() //调用C.baz
    }

    fun caller(d: D) {
        d.foo() //调用扩展函数
    }
}

对于分发接收者和扩展接收者的成员名字冲突的情况,扩展接收者优先。要访问分发接收者的成员你可以使用带标签的this:

class C {
    fun D.foo() {
        toString()          // 调用D.toString()
        this@C.toString()   // 调用C.toString()
    }
}

以下内容仅了解
在类中声明的扩展可以声明为open并在子类中覆写。扩展函数的分发对于分发接收者类型是虚拟的(即动态的),对于扩展接收者类型是静态的:

package cn.szx.kotlindemo

open class D {}
class D1 : D() {}

open class C {
    open fun D.foo() {
        println("D.foo in C")
    }

    open fun D1.foo() {
        println("D1.foo in C")
    }

    fun caller(d: D) {
        d.foo() //调用扩展函数
    }
}

class C1 : C() {
    override fun D.foo() {
        println("D.foo in C1")
    }

    override fun D1.foo() {
        println("D1.foo in C1")
    }
}

fun test() {
    C().caller(D())     // 输出 "D.foo in C"
    C1().caller(D())    // 输出 "D.foo in C1" —— 分发接收者虚拟解析(分发接收着即C还是C1是在运行时决定的)
    C().caller(D1())    // 输出 "D.foo in C" —— 扩展接收者静态解析(扩展接收者即D还是D1是在运行之前决定的)
}
发布了46 篇原创文章 · 获赞 38 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/al4fun/article/details/73928642
03.