Kotlin 笔记

变量 & 常量

var experiencePoints: Int = 5

var 关键字用于声明变量,experiencePoints 是变量名,后面跟 : 及变量的数据类型 Int,然后是赋值。

var 声音的变量是普通变量,val 则是用于声明只读变量,用 val 声明的变量不能重新赋值,类似 Java 当中的 final。多数情况下,建议首选 val

类型推断

上面的例子可以简写作:var experiencePoints = 5,由于 5 是一个已知类型的值,因此编译器会自动推断 experiencePoints 为整数类型。通常情况下,建议尽量依靠类型推断,相对省事。除非因为有歧义而需要手动指定类型。

编译时常量

常量只能在函数外定义,因为函数是在运行时调用的,而常量需要在编译时赋值。定义常量要用到关键字 const,同时和 val 使用。常量名习惯上使用全大写字母。

const val MAX_EXPERIENCE: Int = 5000

fun main(args: Array<String>) {
    // ...
}

Kotlin 到底有没有基本数据类型?

在 Kotlin 中,不存在 Java 那样的基本类型(例如 int),只有引用类型(如 Int)。实际上,在编译过程中,Kotlin 会尽量将引用类型转换成对应的基本类型,以提高性能。但不是所有的引用类型都会被自动转换。

// 这里 value 在编译后是基本类型 int
val value: Int = 42  

// 这里 nullableValue 则是引用类型 Int?
val nullableValue: Int? = null

因此,在语言设计上,Kotlin 没有基本类型。但 IntBooleanLongDoubleFloatShortByteChar 这些类型,在 Kotlin 中常被称为基本类型,这里的“基本类型”是个逻辑概念。

另外,同 Java 一样,String 不是基本类型。

条件语句

if / else

类似 Ruby,在 Kotlin 中,if / else 语句可以返回一个值,因此,可以直接将 if / else 表达式赋值给一个变量。

val healthStatus = if (healthPoints == 100) {
    "is in excellent condition!"
} else {
    "has a few scratches."
}

三元运算

Kotlin 中没有三元运算符,可以用省去括号的单行 if / else 语句代替:

val healthStatus = if (healthPoints == 100) "is in excellent condition!" else "has a few scratches."

in, !in

与 Ruby 类似,Kotlin 也可以用 .. 表示范围。例如 1..5,表示的是从 1 到 5 这个范围。

可以用 in 来判断某个元素是否存在于 Range 类型当中:

if (1 in 1..5) {
    println("1 is in 1..5")
}

in 也可以判断一个元素是否存在其它集合类型当中,包括 Set, List 以及各种 Array 类型。

val list = listOf(1, 2, 3, 4, 5)
if (1 in list) {
    println("1 is in list")
}

另外,还有一个与 in 相反的 !in

when

基本用法

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // 注意这个块
        print("x 不是 1 也不是 2")
    }
}

多个分支同时匹配一个代码块

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

类型检查

val hasPrefix = when(x) {
    is String -> x.startsWith("prefix")
    else -> false
}

同时可以看出,跟 if / else 表达式一样,when 表达式也有返回值。

in!in 一起使用

when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}

同时这个例子展示了 elsewhen 表达式中的用法。

分支中含有多行代码时

when (x) {
    1 -> {
        val y = x + 1
        print("x == 1 and y == $y")
    }
    2 -> print("x == 2")
    else -> print("x is neither 1 nor 2")
}

不带参数的 when

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

函数

Kotlin 社区中,无论是在类的内部还是外部,只要是由 fun 修饰的、带有名字、可复用的代码块,都习惯叫作“函数”。不过这更多只是一个习惯的问题。比如从 Java 转到 Kotlin 的开发者可能会更习惯用“方法”,而函数式编程背景的开发者可能更倾向于用“函数”。

private fun formatHealthStatus(healthPoints: Int, isBlessed: Boolean): String {
    val healthStatus = when (healthPoints) {
        100 -> "is in excellent condition!"
        in 90..99 -> "has a few scratches."
        in 75..89 -> if (isBlessed) {
            "has some minor wounds, but is healing quite quickly!"
        } else {
            "has some minor wounds."
        }
        in 15..74 -> "looks pretty hurt."
        else -> "is in awful condition!"
    }
    return healthStatus
}

以上是一个完全的函数声明示例。

可见性修饰符

示例代码中,private 为可见性修饰符。可选的有:

  • private - 函数只在包含它的文件中可见。
  • protected - 函数在子类中可见。
  • internal - 函数在同一个模块内可见。
  • public - 函数在任何地方都可见。

当可见性修饰符被省去时,默认为 public

思考:是否应该默认都使用 internal 而非 public

internal

internal 是 Kotlin 中比较特别的一个可见性修饰符,由它修饰的函数仅在同一模块内可见。这的“模块”是指一组被一起编译的 Kotlin 文件,通常是同一个 IntelliJ IDEA 模块、Maven 项目或 Gradle 源集。

使用 internal 修饰符意味着你不想让这些声明在模块之外被访问,这有助于封装模块内部的实现细节,同时仍然允许模块内的代码自由地使用这些声明。

例如,如果你正在开发一个库,你可能想要将某些实现细节标记为 internal,这样它们就不会暴露给使用该库的客户端代码,但仍然可以在库内部自由使用。

参数

具名参数

formatHealthStatus(healthPoints: Int, isBlessed: Boolean){}

在调用上面这个函数时,可以用 formatHealthStatus(100, false),也可以用 formatHealthStatus(healthPoints = 100, isBlessed = false)。后者是了“具名参数”的方式,此时可以不按照函数中的参数顺序传递参数,因此也可以写作 formatHealthStatus(isBlessed = false, healthPoints = 100)

默认值参数

fun greet(person: String, greeting: String = "Hello") {
    println("$greeting, $person!")
}

// 调用时可以省略 greeting 参数
greet("Alice")

如果函数的参数中有默认值参数,应该将其放在参数列表的最后。

可变参数

使用 vararg 关键字可以声明一个函数参数,该参数可以接受任意数量的参数。

fun printNumbers(vararg numbers: Int) {
    for (number in numbers) {
        println(number)
    }
}

// 调用时可以传递任意数量的 Int 参数
printNumbers(1, 2, 3, 4, 5)

如果函数的参数中有可变参数,应该将其放在参数列表的最后。

如果同时出来了默认值参数和可变参数,应该将可变参数放在最后,将默认值参数放在可变参数的前面。

fun printMessages(prefix: String = "Info", vararg messages: String) {  
    println("message count: ${messages.size}")  
    for (message in messages) {  
        println("$prefix:$message")  
    }  
}

printMessages(messages = *arrayOf("This is a info message."))
printMessages("Error", "An error occurred.")

单表达式函数

当函数体只有一个表达式时,可以直接将该表达式赋值给函数,简化写法:

private fun auraColor(auraVisible: Boolean): String = if (auraVisible) "GREEN" else "NONE"

// 相当于

private fun auraColor(auraVisible: Boolean): String {
    if (auraVisible) "GREEN" else "NONE"
}

Unit 类型

Kotlin 中使用 Unit 类型作为无返回值函数的返回类型:

fun process(): Unit {
    // 处理逻辑
}

返回值的类型推断

省去返回值类型声明的默认情况下,Kotlin 会将该函数当作无返回值函数,即返回类型为 Unit,不会进行类型推断。因此,如果函数的返回类型不是 Unit,则必须声明返回类型。

但如果函数体只有一行,省去了花括号,此时编译器会进行类型推断,可以省去返回类型的声明。

fun auraColor(auraVisible: Boolean) = if (auraVisible) "GREEN" else "NONE"

Nothing 类型

函数的返回值类型如果是 Nothing,则表示该函数永远无法成功执行。它要么抛出异常,要么因某个原因再也返不回调用处。暂不清楚用处。

函数重载

Kotlin 支持定义 n 个参数不同的同名函数来实现函数重载。

fun performCombat() {
    println("You see nothing to fight!")
}

fun performCombat(enemyName: String) {
    println("You begin fighting $enemyName.")
}

fun performCombat(enemyName: String, isBlessed: Boolean) {
    if (isBlessed) {
        println("You begin fighting $enemyName. You are blessed with 2X damage!")
    } else {
        println("You begin fighting $enemyName.")
    }
}

不过既然 Kotlin 同时提供了“默认值参”的特性,似乎函数重载并没有什么用处。起码在 Ruby 当中没发现需要函数重载的地方。

反引号函数

在 Kotlin 中可以用反引号 `` 来定义或调用以空格和其他特殊字符命名的函数:

fun `**~prolly not a good idea!~**` () {
}
// 可以直接使用 `**~prolly not a good idea!~**`() 调用该函数

通常情况下,不要使用这种方式定义函数。这种方式常用于:

  1. 与 Java 互操作,例如 is 在 Kotlin 中是个关键字,在 Java 中不是,所以 Java 中可能会定义一个 is() 方法,在 Kotlin 当中调用 Java 的 is 方法就可以使用 `is()`
fun doStuff() {
    `is`() // Invokes the Java `is` method from Kotlin
}
  1. 另外,反引号函数可以用来写测试。
fun `users should be signed out when they click logout`() {
    // Do test
}

main 函数

程序运行时的入口

fun main() {
    println("Hello, world!")
}

匿名函数

lambda

Kotlin 中,只要把代码块用一对花括号括起来就是一个 lambda 了:

{
    val n = 9
    println(n)
}

可以直接调用它:

{
    val n = 9
    println(n)
}()

也可以把它赋值给一个变量然后调用:

val f = {
    val n = 9
    println(n)
}
f()

lambda 靠最后一行代码的值隐式决定函数的返回值。

println({
    val currentYear = 2021
    "Welcome to SimVillage, Mayor!(copyright $currentYear)"
})

这里加了 return 反而会因为语法错误无法编译。

函数类型

lambda 可以赋值给变量:

val greetingFunction:() -> String = {
    val currentYear = 2018
    "Welcome to SimVillage, Mayor!(copyright $currentYear)"
}
println(greetingFunction())

lambda 也有类型,叫做“函数类型”,由参数、返回值决定。上面的代码可以跟 val number: Int = 9 对比着看,代码中的 () -> String 就表示该函数的类型。

当然,示例代码中的类型声明也可以省去,把这个工作交给类型推断。

lambda 的参数

lambda 可以带参数,定义的方式是在左花括号的后面写上参数名、类型名与箭头符号:

val greetingFunction = { playerName: String ->
    val currentYear = 2018
    "Welcome to SimVillage, $playerName!(copyright $currentYear)"
}
println(greetingFunction("Guyal"))

it 关键字

当 lambda 只有 1 个参数时,可以省去该参数,并用 it 关键字来访问该参数。但这种情况下,参数的定义省去了,就无法推断出 lambda 的类型,因此得把 lambda 的类型声明加上:

val greetingFunction: (String) -> String = {
    val currentYear = 2018
    "Welcome to SimVillage, $it!(copyright $currentYear)"
}

把 lambda 当作参数

lambda 可以当作参数传给另一个函数:

fun runSimulation(playerName: String, greetingFunction: (String, Int) -> String) {
    val numBuildings = (1..3).shuffled().last()
    println(greetingFunction(playerName, numBuildings))
}

runSimulation("yuan", { playerName: String, numBuildings: Int ->  
    val currentYear = 2024  
    println("Adding $numBuildings houses")  
    "Welcome to SimVillage, $playerName! (copyright $currentYear)"  
})

如果 lambda 是函数的最后一个参数,或者函数只有 1 个 lambda 参数,可以把 lambda 从参数的圆括号中拿出来:

runSimulation("yuan") { playerName: String, numBuildings: Int ->  
    val currentYear = 2024  
    println("Adding $numBuildings houses")  
    "Welcome to SimVillage, $playerName! (copyright $currentYear)"  
}

函数内联

lambda 会以对象实例的形式存在,Kotlin 会将它编译成一个匿名类的实例,这需要消耗内存。同时,在 lambda 中访问外部变量时,这些变量会作为成员存储在 lambda 对象中,因此 JVM 会为所有同 lambda 打交道的变量分配内存。lambda 的内存开销可能会带来严重的性能问题,Kotlin 为此引入了一种优化机制叫内联。在带有 lambda 参数的函数前面使用 inline 关键字修饰,就可以将 lambda 代码内联进该函数。

inline fun runSimulation ...

有了 inline 关键字后,哪里需要使用 lambda,编译器就会将函数体内的代码复制粘贴到哪里,这样一方面省去了函数调用的开销,一方面也节省下了不断创建对象所需要消耗的内存。(客户端开发和服务器端开发在细节上需要考虑的真不一样,连一个函数调用的开销都要节省)

代价

虽然 inline 可以减少内存开销,但与此同时,因为(编译后的)代码量的增加,软件的体积会有所增长。因此在决定是否使用 inline 时,需要权衡函数的调用频率函数体的大小性能要求代码体积限制

函数引用

具名函数(由关键字 fun 定义的函数)可以通过 :: 操作符来引用,当作值参传递给另一个函数。

fun printConstructionCost(numBuildings: Int) {
    val cost = 500
    println("construction cost: ${cost * numBuildings}")
}
runSimulation("Guyal", ::printConstructionCost, greetingFunction)

凡是使用lambda表达式的地方,都可以使用函数引用。

返回函数

函数也可以作为另一个函数的返回值:

fun returnFunction(): (Int) -> Unit {
    return ::printConstructionCost
}

空指针

可空类型

Kotlin 默认类型变量不可以赋值为 null,可空类型需要在类型名称后面加问号:

var notNull: String = null // 无法编译
var nullable: String? = null

安全调用

Kotlin 不允许你在可空类型值上调用绝大多数函数。

var nullable: String? = null
nullable.uppercase() // 无法编译

除非你主动接手安全管理。

var nullable: String? = null
if (nullable != null) {
  nullable.uppercase() // 编译通过
} 

?. 操作符

?. 叫作“安全调用操作符”。通过 ?. 操作符调用变量的方法时,如果变量的值为 null,则直接返回 null

var nullable: String? = null
nullable?.uppercase() // 返回 null

实际上,通过 ?. 调用函数,返回的是原函数返回类型对应的可空类型。

val a: IntArray? = null  
  
val s = a?.joinToString(",")?.split(",")  
  
println(s)

在这个示例中,IntArray.joinToString() 返回的是 String 类型的对象,所以 a?.joinToString(",") 返回的是 String? 类型的对象,因此可以在后面接着通过 ?. 调用 String 的成员函数 split()

使用带 let 的安全调用

有时候,可以用 ?.let 创建一个保证非空的上下文环境,省略掉频繁的 ?. 输入:

val beverage = readlnOrNull()?.let {  
    if (it.isNotBlank() ) {  
        it.uppercase()  
    } else {  
        "Buttered Ale"  
    }  
}  
  
println(beverage)

这里如果 readlnOrNull() 返回 null,后面的 let() 函数不会被调用,所以如果 let() 函数被调用,那么 readlnOrNull() 返回的一定不是 null,因此在 let() 函数的 lambda 内部可以安全地直接调用 it 的所有成员函数。

!!. 操作符

!!. 操作符忽略空值判断,不管对象是否为空直接调用函数。如果此时对象为空,则会抛出异常。

var nullable: String? = null
nullable!!.uppercase() // 抛出异常

该操作符会使程序失去空安全保护,因此应尽量避免使用。

?: 操作符

?: 叫作“空合并操作符”,当它左边的求值结果为 null 时,返回右边的值。

var s = null ?: "hello"
// s => "hello"

Range

前面提到过 Range 的基本用法。1..5,表示的是从 1 到 5 这个范围,这个范围包含了 5。也可以写成 1.rangeTo(5)

如果不想要这个范围包含 5,则需要用 1 until 5

想把 1..5 反过来表示从 5 到 1 的话,则要用 5 downTo 1,其中包含了 1。

1 until 5 则没有倒过来的写法,需要写成 4 downTo 1

val range = 1..5,这里 range 的类型是 IntRange。常见的还有 LongRange, CharRange 等。

异常

抛出异常

最普通的用法:

throw RuntimeException("something wrong")

在 Kotlin 当中,throw 语句是一个表达式,意味着它可以这样使用:

val s = person.name ?: throw IllegalArgumentException("Name required")

throw 返回的是一个 Nothing 类型的值。

Throwable 是所有异常的父类。

捕获/处理异常

在 Kotlin 里,try 是个表达式,这意味着它有个返回值。它的返回值是 try 块里的最后一行代码的返回值,或者是 catch 块里的最后一行代码的返回值。这二者不可能同时存在,因为如果 try 能正常执行完毕,就没有 catch 什么事了。

finally 块不对返回值有任何影响。

val a: Int? = try { input.toInt() } catch (e: NumberFormatException) { null }

每个异常都带有一条 message,一个 stack trace。而 cause (引发该异常的异常)有可能是 null

try {  
    1 / 0  
} catch (e: Exception) {  
    println(e.message)  
    println(e.stackTrace.joinToString("\n"))  
    println(e.cause)  
}

Unchecked Exception

在 Kotlin 中,所有异常都是 Unchecked Exception。也就是说,Kotlin 不强制对异常进行捕获和处理。

自定义异常

自定义异常只需要定义一个类,继承自另一个异常就可以了。

class UnskilledSwordJugglerException() : IllegalStateException("Player cannot juggle swords")

字符串

字符串模板

一般的字符串拼接:

val name = "yuan"
val healthStatus = "is in excellent condition!"

println(name + " " + healthStatus)

使用字符串模板的方式拼接字符串:

// ...
println("$name $healthStatus")

如果在其中插入的不是变量,而是一个表达式,可以使用 ${}

val isBlessed = true
println("(Blessed: ${if (isBlessed) "YES" else "NO"})")

标准函数库

apply()

使用 apply() 函数可以省掉重复的函数接收者。

val menuFile = File("menu-file.txt").apply {
    setReadable(true)
    setWritable(true)
    setExecutable(false)
}

// 以上代码相当于

val menuFile = File("menu-file.txt")
menuFile.setReadable(true)
menuFile.setWritable(true)
menuFile.setExecutable(false)

let()

apply() 类似,但会把接收者传入 lambda 的参数。

val firstItemSquared = listOf(1,2,3).first().let { i ->
    // 这里也可以把 i 参数省去,改用 it
    i * i
}

还有一点不同的是,let() 返回的是 lambda 的返回值,而 apply() 返回的是接收者自身。

run()

apply() 类似,但 run() 返回的是 lambda 的返回值,且不往 lambda 传入接收者。

val menuFile = File("menu-file.txt")
val servesDragonsBreath = menuFile.run {
    readText().contains("Dragon's Breath")
}

with()

with()run() 的变体,它们的功能和行为是一样的,但是调用的方式有所不同。with() 需要将接收者作为第一个参数传入。

val menuFile = File("menu-file.txt")
val servesDragonsBreath = with(menuFile) {
    readText().contains("Dragon's Breath")
}

also()

also()let() 类似,区别是 also() 返回接收者本身,而 let() 返回 lambda 的返回值。

var fileContents: List<String>
File("file.txt")
        .also {
            print(it.name)
        }.also {
            fileContents = it.readLines()
        }
}

takeIf()

当 lambda 返回值为 true 时,takeIf() 返回接收者对象本身,否则返回 null

val fileContents = File("myfile.txt")
    .takeIf { it.canRead() && it.canWrite() }
    ?.readText()

以上代码在文件可读可写时,才读取文件内容。

takeUnless()

takeIf() 相反。

List

创建不可变列表

listOf() 可以用来创建一个不可变列表 List<T>,也就是说创建出来的列表不可以增加、删除元素:

listOf("FA20", "R18A1", "M15A", "F20C")

创建可变列表

如果要创建一个可变列表 MutableList<T>,可以用 mutableListOf()。它们之间可以使用 toList()toMutableList() 互相转换。

可变列表可以使用 add()remove()removeIf()addAll()[]=+=-=clear() 等函数来修改。

获取列表元素

下标访问

List 可以像数组一样直接通过下标访问其中的元素。

val engines = listOf("FA20", "R18A1", "M15A", "F20C")
println(engines[0])

当然,越界访问的话,会抛出 ArrayIndexOutOfBoundsException 异常。

getOrElse() / getOrNull()

getOrElse() 函数可以用于处理越界异常情况。getOrNull() 则在越界时直接返回 null

val patronList = listOf("Eli", "Mordoc", "Sophie")
patronList.getOrElse(4) { "Unknown Patron" }

first() / last()

List 还提供了 first()last() 函数,用于获取第 1 个和最后一个元素。

解构赋值

List 集合支持以解构的方式获取其中的前 5 个元素。如果尝试获取 6 个,则编译无法通过。

val (a, b, c, d, e) = (0..9).toList()
// val (a, b, c, d, e, f) = (0..9).toList() 无法编译

遍历

遍历列表可以用 for (... in ...) 语法,也可以用列表的 forEach() 函数。一个比较大的区别是,前者可以用 breakcontinue 来控制循环的中断或跳过,后者无法使用这两个关键字。

数组

Kotlin 中,除了 List<T> 外,还有 Array<T> 以及各种基本类型的数组类型(例如 IntArray)。

Kotlin 创建了一系列数组类型,来对应 Java 当中的 基本数据类型数组:

  • IntArray 对应 int[],创建函数是 intArrayOf()
  • DoubleArray 对应 double[],创建函数是 doubleArrayOf()
  • long, short, byte, float, boolean 以此类推。

List<Int>Array<Int>IntArray 为例来作对比:

  • IntArray 存储的是基本类型,其它两者存储的都是引用类型;
  • Array<Int>IntArray 长度都是固定的,而可变形式的 List,MutableList<Int> 长度是可变的。

就使用场景来说,IntArray, ByteArray 这样的基本类型多用于与 Java API 交互。

Array<T> 的创建函数是 arrayOf()。基本数据类型数组(例如 IntArray)可以调用 toTypedArray() 函数转换为 Array<T>

最后,函数的 vararg 参数类型为 Array<T>

Set

Set 的特点是内部的元素都具有唯一性,不存在重复元素。并且 Set 内部的元素是没有固定的顺序的。虽然 Set 也能通过 elementAt(Int) 获取到元素,但速度比 List 慢得多,如果需要通过下标直接获取元素,尽量使用 List。

与 List 一样,setOf() 可以创建无重复元素的集合 Set<T>mutableSetOf() 可以创建集合 MutableSet<T>

Map

创建 Map

mapOf() 用于创建 Map<T>。与 SetList 一样,Map<T> 是不可变的集合类型,如果需要创建可变的集合类型,可以使用 mutableMapOf() 函数创建 MutableMap<T>

val map = mapOf("Eli" to 10.5, "Mordoc" to 8.0, "Sophie" to 5.5)

这里的 to 实际上是个函数,也可以写作 "Eli".to(10.5),用于创建一个键值对 Pair 实例(相当于 Ruby 的 "Eli" => 10.5)。当然,还可以用直接构造 Pair 的方式:

val map = mapOf(Pair("Eli", 10.5), Pair("Mordoc", 8.0), Pair("Sophie", 5.5))

取值

Map 的取值用 [],也可以用 getValue(),前者取不到值返回 null,后者会抛出异常 NoSuchElementException

map["Eli"] // 从 map 里取值,取不到则返回 null
map.getValue("Eli") // 从 map 里取值,取不到则抛出异常

另外还有 getOrDefault(),取不到值时会返回指定的默认值。

patronGold.getOrDefault("Reginald", 0.0) // 取不到值时返回 0.0

getOrElse()getOrDefault() 相似,区别在于它是用 lambda 返回默认值。

patronGold.getOrElse("Reggie") {"No such patron"} // 取不到值时返回 "No such patron"

修改

使用 []= 可以修改 Map 的值,如果键值对原本不存在,则是添加一个值。

val mutableMap = mutableMapOf("Eli" to 10.5, "Mordoc" to 8.0, "Sophie" to 5.5)
mutableMap["Mark"] = 10.0 // 修改或添加元素

put() 函数与 []= 作用一样。

val mutableMap = mutableMapOf("Eli" to 10.5, "Mordoc" to 8.0, "Sophie" to 5.5)
mutableMap.put("Mark", 10.0) // 修改或添加元素

+= 可以往 Map 里追加键值对,支持单个 PairList<T>Map<T>

val patronGold = mutableMapOf("Mordoc" to 6.0)
patronGold += "Eli" to 5.0
patronGold += listOf("Sophie" to 5.5)
patronGold += mapOf("Mark" to 10.0)

putAll() 函数和 += 差不多,但它不支持单个 Pair 作为参数。

val patronGold = mutableMapOf("Mordoc" to 6.0)
patronGold.putAll("Eli" to 5.0) // 这里无法编译
patronGold.putAll("Sophie" to 5.5)
patronGold.putAll(mapOf("Mark" to 10.0))

-=+= 相反,用于删除 Map 里的键值对:

val patronGold = mutableMapOf("Mordoc" to 6.0)
patronGold -= "Mordoc"

- 也用于删除 Map 里的键值对,但是它并不修改原 Map,而是返回一个新的 Map,其中少了被删除的键值对:

val patronGold = mutableMapOf("Mordoc" to 6.0)
val newPatronGold = patronGold - "Mordoc"

序列 (Sequence)

对一个集合进行遍历,通常需要先通过一系列计算,获得整个集合,然后再遍历。

fun simple(): List<Int> = listOf(1, 2, 3)
 
fun main() {
    simple().forEach { value -> println(value) } 
}

而使用序列可以在生成每个元素的过程中,插入对元素的操作:

fun simple(): Sequence<Int> {
    return sequence { // sequence builder
        for (i in 1..3) {
            Thread.sleep(1000) // pretend we are computing it
            yield(i) // yield next value
        }
    }
}

fun main() {
    simple().forEach { value -> println(value) }
}

这里 simple() 函数里 sequence() 内部的代码块不会立即执行,直到 main() 函数里调用 forEach() 时才会执行。

创建 Sequence

sequenceOf()

可以通过 sequenceOf() 函数直接创建一个固定长度的 Sequence。

val numbers = sequenceOf(0, 1, 2, 3, 4)

asSequence()

也可以通过 IterableasSequence() 函数创建 Sequence。

val numbers = listOf("one", "two", "three", "four")
val numbersSequence = numbers.asSequence()

除了常见的集合类型以外,字符串和文件也都实现了 Iterable 接口。

generateSequence()

使用 generateSequence() 可以根据 lambda 的结果动态生成 Sequence。它的第一个参数叫作种子,也是序列生成的第一个元素。它可以是具体的值,也可以是 null。如果为 null 则该序列为空。

第二个参数是计算下一个元素的 lambda,这个 lambda 又以前一个元素为参数。

val oddNumbers = generateSequence(1) { it + 2 } // `it` is the previous element
println(oddNumbers.take(5).toList())

上面的 oddNumbers 在使用时如果不加限制,会无限生成奇数直到 JVM 堆溢出,如果想加以限制,在条件满足时返回 null 即可:

val oddNumbersLessThan10 = generateSequence(1) { if (it < 8) it + 2 else null }

sequence()

sequence() 结合 yield()yieldAll() 函数,可以更灵活地定制化生成逻辑,灵活组合数据源。其中 yield()yieldAll() 用于生成序列中的元素。

val oddNumbers = sequence {
    yield(1)
    println("a")
    yieldAll(listOf(3, 5))
    println("b")
    yieldAll(generateSequence(7) { it + 2 })
    println("c")
}

println(oddNumbers.take(2).toList())

可以尝试把 oddNumbers.take(2) 里边的参数替换成 1,3,5 等数字,观察输出。

操作 Sequence

对比下面两个函数

val LIST = listOf(0, 1, 2, 3, 4)
val SEQUENCE = sequenceOf(0, 1, 2, 3, 4)

fun getFirstFromList() {
    LIST
        .map { println("map $it"); it * 5 }
        .filter { println("filter $it"); it % 2 == 0 }
        .take(1)
        .forEach { println("list $it") }
}

fun getFirstFromSequence() {
    SEQUENCE
        .map { println("map $it"); it * 5 }
        .filter { println("filter $it"); it % 2 == 0 }
        .take(1)
        .forEach { println("sequence $it") }
}

getFirstFromList() 中,map() 会产生一个结果,filter() 会产生一个结果,take(Int) 会产生一个结果,每次操作都是针对一个新的集合对象。这里的 map()filter() 会各执行 5 次。

getFirstFromSequence() 里的 map() , filter()take(Int) 操作的则是同一个对象。其实这里的 map()filter() 等函数与前面 List<T> 的实现完全不同,是 Sequence 的实现,只是函数名字相同。在这个例子里, map()filter() 只执行了 1 次。因为 filter 出来第一个元素再 take 1 之后,已经可以返回了,后续的代码不需要再执行。

中间操作和终结操作

对于 Sequence,map()filter()take()drop() 等操作叫作“中间操作”,又叫“惰性操作”。toList()toSet()first()forEach() 等操作叫作“终结操作”。只有终结操作会最终触发计算的执行。

创建类与实例化

class Player {
    fun castFireball(numFireballs: Int = 2) = println("A glass of Fireball springs into existence. (x$numFireballs)")
}

val player = Player()
player.castFireball()

以上代码定义了一个 Player 类,其中有一个 castFireball() 作为成员函数。Kotlin 没有 new 关键字,而是在类名后面加上括号来调用构造函数创建实例。

类属性

与普通变量的声明一样,属性也是通过 valvar 关键字来区别是否可变。但是属性必须有初始值。

class Player {
    var name = "madrigal" // var 表示该属性可变
    // val name = "madrigal"  <- val 表示该属性不可变
    // var name: String?  <- 无法编译,属性必须有初始值。即使是 nullable 也得赋值为 null
}
val player = Player()
println(player.name)

属性的声明

属性可以在类的内部声明,也可以在主构造函数里声明,但不能在两处同时声明。

// 在类的内部声明
class Player(name: String) {
    var name = name
}

// 在主构造函数里声明
class Player(var name: String) // 这样跟上面是等效的

// 不可以在两处同时声明
class Player(var name: String) {
    var name = name // 这样会无法编译
}

field, getter 和 setter

属性在定义时自动生成了一个 field,一个 getter 函数,可变属性(用 var 定义的属性)还会有 setter 函数。

这里的 field 又叫 backing field,也就是隐藏在 getter/setter 后面真实的字段,它只能在 getter 和 setter 内部访问。

以前面的代码为例,实际上 Kotlin 为 name 属性生成了一个 field,一个 setter 和 getter。默认情况下,setter 和 getter 只是对该属性的简单读写。

在需要的时候,你可以重新定义 setter 和 getter。

class Player {
    var name = "madrigal" // 这里如果用 val,编译时会报错,因为只读属性不能定义 setter
        get() {
            return field.uppercase()
        }
        set(value) {
            field = value.trim()
        }
}
val player = Player()
player.name = "  penny  " // 空格会被 setter 去掉
println(player.name) // getter 把 field 转换成了大写,输出 PENNY

属性的可见性

同函数一样,属性默认情况下也是 public 的。getter 的可见性必须与属性本身一致,setter 的可见性可以修改,但不能比属性的可见性范围更大。

class Player {
    var name = "madrigal" 
        get() { // getter 的可见性只能跟属性保持一致
            return field.uppercase()
        }
        private set(value) {  // 可以修改 setter 的可访问性
            field = value.trim()
        }
}

如果纯粹是想隐藏 setter 而不改变其原始行为可以写作private set

class Player {  
    var name = "madrigal"  
        get() {  
            return field.uppercase()  
        }  
        private set
}

计算属性

可以单纯通过 getter 和 setter 来定义属性,不需要为其初始化,也就不需要 field。这种属性叫作计算属性。计算属性必须有 getter,可以没有 setter。

class Dice() {
    val rolledValue
        get() = (1..6).shuffled().first()
}
val dice = Dice()
println(dice.rolledValue)
println(dice.rolledValue)
println(dice.rolledValue)

防范竞态条件(race condition)

class Weapon(val name: String)
class Player {
    var weapon: Weapon? = Weapon("Ebony Kris")

    fun printWeaponName() {
        if (weapon != null) {
            println(weapon.name)
        }
    }
}

以上代码无法编译通过,因为在 Player 内部 weapon 是个可变的属性,对 weapon 进行空值判断到打印名字之间,weapon 还有可能被其它线程修改为空值。此时应该用如下写法:

class Weapon(val name: String)
class Playerr {
    var weapon: Weapon? = Weapon("Ebony Kris")

    fun printWeaponName() {
        weapon?.also { println(it.name) }
    }
}

这里的 it 是局部变量,不用担心被外部修改。

也可以手动地把要检查的变量保存为局部变量再进行操作:

class Playerr {
    var weapon: Weapon? = Weapon("Ebony Kris")
    fun printWeaponName() {
        val currentWeapon = weapon
        if (currentWeapon != null) {
            println(currentWeapon.name)
        }
    }
}

初始化

主构造函数

类的声明本身包含了主构造函数:

class Player (name: String, healthPoints: Int, isBlessed: Boolean, isImmortal: Boolean){
    var name = name
    var healthPoints = healthPoints
    var isBlessed = isBlessed
    var isImmortal = isImmortal
    ....
}

这里的 class Player (name: String, healthPoints: Int, isBlessed: Boolean, isImmortal: Boolean) 就隐含了构造函数。

构造函数参数可以同时声明为属性,只要在前面加上 valvar

class Player (var name: String, var healthPoints: Int, var isBlessed: Boolean, var isImmortal: Boolean)

init 块

初始化代码可以放在 init 块中,实际上,可以将 init 块视为主构造函数的函数体。

class Player (var name: String, var healthPoints: Int, var isBlessed: Boolean, var isImmortal: Boolean) {
    init {
	    println("初始化:$name")
    }
}

init 块适合放置需要复杂计算的初始化代码,如果只是简单地给属性赋值,直接在主构造函数里声明即可。

一个类可以有多个 init 块,它们会按照在类中出现的顺序执行。但通常这意味着可以将其合并。

次构造函数

class Player (var name: String, var healthPoints: Int, var isBlessed: Boolean, var isImmortal: Boolean){
    var town = "Bavaria"

    constructor(name: String) : this(name, 100, true, false) {
         town = "The Shire"
    }
}

这里的 constructor() 就是次构造函数,相当于构造函数重载。次构造函数要么直接调用主构造函数,要么通过其它次构造函数调用主构造函数。

在上面的例子中,次构造函数通过 : this(name, 100, true, false) 调用了主构造函数。

次构造函数里不能定义属性。

延迟初始化

默认情况下 Kotlin 要求在构建类的实例时,所有属性都必须完成初始化。但用 lateinit 可以绕开它,延迟属性的初始化。初始化后与其它属性在使用上无任何不同。

class Player {
    lateinit var alignment: String
}

上面的代码如果去掉 lateinit 是无法通过编译的,但这时候必须由开发者来保证该属性在使用之前被初始化。否则会抛出运行时异常 UninitializedPropertyAccessException

判断属性是否初始化

使用 :: 可以获取到属性或者函数的引用,再通过 isInitialized 属性可以可以判断属性是否初始化过了。

class Player {  
    lateinit var alignment: String  
  
    fun check() {  
        println(::alignment.isInitialized)  
    }  
}

惰性初始化

lazy() 这个函数可以创建一个 Lazy<T> 对象,它接受一个 lambda 作为参数,传递给 lazy 的 lambda 会在第一次尝试取值的时候执行。Lazy<T> 是一个“委托类”(详见委托),在 Kotlin 中,可以通过 by 关键字获取委托类的实例的值。

class Player {
    val alignment: String by lazy {
        println("lazy")
        "GOOD"
    }
}

val player = Player()
println("player created")
println(player.alignment)
println(player.alignment)

在上面这个例子中,Player.alignment 的值在首次访问时才被赋予,并且会被缓存起来,第二次访问时,lambda 不再执行。

嵌套的类

类是可以嵌套的,如果你可以在一个类的内部嵌套定义另一个类。

class A {
    class B{
    }
}

如果要让 B 被限制只能在 A 内使用,还可以用 private 等可见性修饰符来修饰。

数据类

Kotlin 中有一种类型叫作 “数据类”,其实就是值类型,用 data class 定义。

数据类重写了 Anyequals() 函数,只要值相同,== 比较都返回 true。数据类的 toString()hashCode() 也重写过。另外提供了一个 copy() 函数,用来创建内容完全一样的新实例。

data class Coordinate(val x: Int, val y: Int)

一个类要成为数据类,要符合一定条件。总结下来,主要有三个方面:

  • 数据类必须有至少带一个参数的主构造函数;
  • 数据类主构造函数的参数必须是 valvar
  • 数据类不能使用 abstractopensealedinner 修饰符。

枚举类

Kotlin 的枚举用 enum class 定义。

enum class Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST
}
Direction.EAST // usage
Direction.valueOf("EAST") // usage

可以像普通类一样定义构造函数和成员函数(注意代码中的分号)。

data class Coordinate(val x: Int, val y: Int)

enum class Direction(private val coordinate: Coordinate) {
    NORTH(Coordinate(0, -1)),
    EAST(Coordinate(1, 0)),
    SOUTH(Coordinate(0, 1)),
    WEST(Coordinate(-1, 0)); //注意分号

      
    fun updateCoordinate(playerCoordinate: Coordinate) {  
        this.coordinate = Coordinate(playerCoordinate.x, playerCoordinate.y)  
    }
}

Direction.EAST.updateCoordinate(Coordinate(1, 0)) // usage

密封类

Kotlin 中有个密封类的概念,用 sealed 来声明。

sealed class Result {
    data class Success(val data: String): Result()
    data class Error(val message: String): Result()
    object Loading: Result()
}

密封类的特点:

  • 所有子类必须在同一个文件中声明
  • 密封类本身是抽象的
  • 在 when 表达式中处理所有情况时,不需要 else 分支

继承

类声明语句后面的 : 操作符用于继承。继承操作符后面作为父类的类名必须跟着一对小括号,代表要调用的父类构造函数。

所有的类默认是不可继承 (final) 的,如果要把一个类当作父类,必须使用 open 关键字修饰。

一个类只能有一个父类,无法继承多个类。

open class Room(val name: String)
class TownSquare: Room("Town Square")  // 相当于调用 super("Town Square")

这里的 : Room("Town Square") 表示 TownSquare 继承了 Room 这个类,并且 TownSquare 在构造时会把 "Town Square" 这个参数传递给 Room 的构造函数。

覆盖父类的函数

默认情况下,父类的函数是无法覆盖 (final) 的,如需被覆盖,要用 open 关键字修饰,同时用 override 关键字修饰子类的方法。

如果父类的方法可见性为 protected 或 private,并在覆盖时没有特别指定,那么子类的方法可见性跟父类保持一致。但不允许将子类方法的可见性范围改得比父类同名方法小。

open class Room(val name: String) {
    fun description() = "Room: $name"
    open fun load() = "Nothing much to see here..."
}

class TownSquare(name: String) : Room(name) {
    override fun load() = "The villagers rally and cheer as you enter!"
}

子类覆盖了父类的函数之后,子类的该函数默认是可覆盖的(不需要 open),如果不想被覆盖,需要加 final 修饰。

覆盖父类的属性

属性的覆盖和函数一样,需要 openoverride 关键字

open class Room(val name: String) {
    protected open val dangerLevel = 5 // protected 可在子类中访问
}

class TownSquare(name: String) : Room(name) {
    override val dangerLevel = super.dangerLevel - 3
}

抽象类

抽象类用 abstract 修饰,类内部的函数可以有函数体,也可以没有。没有函数体的函数需要用 abstract 修饰,默认 (也必须) 是 open 的、public 的。

有函数体的函数可以不是 public 的,且默认是 final(不可覆盖)的。也可以用 open 修饰,变为可覆盖的。

abstract class Animal {
    abstract fun eat()

    open fun defecate() {
        // ...
    }
}

class Human : Animal() {
    override fun eat() {
        println("human eat")
    }

    override fun defecate() {
        println("human defecate")
    }
}

属性也可以是抽象的。父类中定义了抽象属性,子类中就一定得有这个属性。

abstract class SuperClass {
    abstract var name: String // 因为需要被继承,所以不能是 private
}
class MyClass : SuperClass() {
    override var name: String = "6"
}

类型检查

is 可用于类型检查。

abstract class Animal 
class Human : Animal()

val cow = Animal()
val bob = Human()

bob is Animal // -> true
bob is Human // -> true
cow is Human // -> false

类型转换

Kotlin 的 as 可以用于将变量的类型强制转换为某个类型,前提是该变量指向的对象确实属于指定的类型。如果类型不匹配,将会转换失败。

fun printIsSourceOfBlessings(any: Any) {  
    val isSourceOfBlessings = if (any is Player) {  
        any.isBlessed  
    } else {  
        (any as Room).name == "Fount of Blessings"  
    }  
    println("$any is a source of blessings: $isSourceOfBlessings")  
}

如果往 printIsSourceOfBlessings() 传入 PlayerRoom 以外的类型,会在运行时抛出 ClassCastException

智能类型转换

同时,该示例中存在着隐式的自动类型转换,在条件表达式的第一个分支中,经过 is 的判断,anyPlayer 类型,因此在该分支的代码块中可以直接访问 Player 的属性,而无需手动地转换类型。

Any

在 Kotlin 中,Any 是所有非空类型的父类,Any? 是所有可空类型的父类。

Any 内部实现了 hashCode()equals()toString() 等函数的通用版本。

接口

实现接口使用的操作符与继承相同,但接口没有构造函数,所以接口后边不需要加括号。

一个类可以实现多个接口,接口名之间用逗号分隔。

Kotlin 的接口内部可以实现函数,也可以不实现(就是抽象的)。

interface Runnable {
    fun run()
}

interface Creativity {
    fun createThings()

    fun doSomething() {
        println("do something")
    }
}

class Human : Runnable, Creativity { // 这里没有小括号
    override fun run() {
        // ...
    }

    override fun createThings() {
        // ...
    }
}

接口中声明的所有抽象函数的可见性都只能是 open public 的,修饰符可省略。

接口的属性声明

Kotlin 的接口可以声明属性,这点与 Java 不同,因为 Kotlin 的属性在外部看来就是 getter 和 setter 函数,所以在接口上声明属性,也就相当于声明了它的 getter 和 setter 的抽象函数。

interface User {
    // 可以有抽象属性
    var gender: Int
}

同时,因为 Kotlin 的接口可以有函数实现,所以也可以有属性 getter 函数的实现,但前提是该属性是用 val 声明的。因为接口本身是没有状态的,用 val 声明,Kotlin 可以认为这个 getter 的背后不一定有 backing field,如果用 var 声明,则必然有 backing field,getter 和 setter 要共同维护这个 field。

interface User {
    // 声明了属性
    var gender: Int

    // 可以有 getter 的实现
    val name: String
        get() = "default"

    // 不能有 setter 的实现
    var age: Int
        get() = 18 // 编译错误
        set(value) { } // 编译错误
}

实际上,即使是 val 声明的属性,如果在接口中提供了 getter 实现,在该 getter 的代码中也不能访问 field,否则会编译出错。

interface User {
    // 可以有 getter 的实现
    val name: String
        get() = field.uppercasse() // 无法编译

}

与抽象类的区别

本质上,接口只定义行为,而抽象类包含了部分的实现。因此

  • 接口不能有状态,抽象类可以有;
  • 接口没有构造函数,抽象类可以有;
  • 接口不能有初始化代码,抽象类可以有。

函数名冲突

某子类继承了一个父类的同时,实现了另一个接口,如果接口与父类里有同名非抽象函数,则子类必须提供自己的实现。

如果子类内部调用了父类或者接口的同名函数,需要以 super<父类/接口>.metho() 的形式调用,并且父类中的同名方法需要以 open 修饰。

interface InterfaceA {
    fun methodA() {
        println("method a in interface a")
    }
}

abstract class Super {
    open fun methodA() {
        println("method a in abstract class")
    }
}

class A : Super(), InterfaceA {
    override fun methodA() {
        super<InterfaceA>.methodA()
        println("method a in class a")
    }
}

函数式接口(SAM)

  • 当 Kotlin 的接口里只有一个函数,并且该接口被 fun 修饰时,该接口称为函数式接口,或 SAM (Single Abstract Method) 接口,在使用它时,可以直接把 lambda 传递给该接口进行构造:
fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

fun main() {
    val isEven = IntPredicate { it % 2 == 0 }
}
  • 如果某方法的最后一个参数是 SAM 接口,可以把接口名称省去:
fun interface Runnalbe {
    fun run()
}

fun interface Catchable {
    fun catch()
}

fun relayRace(runner: Runnalbe, catcher: Catchable) {
    runner.run()
    catcher.catch()
}

class Runner : Runnalbe {
    override fun run() {
        println("run as a runner")
    }
}

fun main() {
    val runner = Runner()

    relayRace(runner) {
        println("catching stick")
    }
}
  • is 运算符用于类型检测
room is Room
  • Kotlin 当中,所有类都有一个父类叫 Any,类似 Java 里的 Object
  • as 用于强制类型转换
fun castToRoom(any: Any): Room {
    return any as Room
}
  • is 检测对象的类型之后,如果结果为 true,该对象会被自动转换成被检测的目标类型。
fun printIsSourceOfBlessings(any: Any) {
    if (any is Player) {
        println(any.isBlessed) // Any 没有 isBlessed 属性,这里已经转成了 Player
    }
}

内部类和嵌套类

class Outer {
    var outerAttr = 1
    
    class StaticInner {
        fun fun1() {
            println(outerAttr) // 无法访问
        }
    }
    
    inner class Inner {
        fun fun1() {
            println(outerAttr) // 可以访问外部类的成员
            println(this@Outer)
        }
    }
    
    fun fun1(){
        Inner()
        StaticInner()
    }
}

fun main() {
    var outer = Outer()
    var inner1 = Outer.Inner() // 无法构造
    var inner2 = Outer().Inner() // 可以通过外部类的实例构造
    var staticInner = Outer.StaticInner()
}
  • 类与接口都可以互相嵌套;
  • inner 修饰的类称为内部类,可以访问外部类的成员,并且有一个外部类对象的引用,可通过 this@Outer 来访问;

对象

对象声明(单例类)

对象声明通过关键字 object 来创建一个单例类。方法就是将普通的类声明时的关键字 class 替换成 object

object Game {
    fun play() {
        while(true) {
            // runing...
        }
    }
}

Game.play() // 用法

跟声明类型一样,对象声明也可以继承其它类和实现其它接口。

对象表达式(匿名内部类)

对于接口、抽象类或者普通 open class,可以使用对象表达式:

val abandonedTownSquare = object : TownSquare() {
    override fun load() = "You anticipage applause, but on one is here..."
}

对象表达式常用于创建匿名内部类。

当然通常情况下,对象表达式只会用于接口和抽象类,非抽象的普通 open class 不会这么使用。

伴生对象

伴生对象使用 companion 关键字,定义在类的内部,可以实现类似 Java 中的静态方法。但实际上 Kotlin 是创建了另外一个对象,并保证只存在一个对象实例。

class PremadeWorldMap {
    companion object {
        private const val MAPS_FILEPATH = "nyethack.maps"
        fun load() = File(MAPS_FILEPATH).readBytes()
    }
}
PremadeWorldMap.load() // usage

以上方式创建的“静态方法”不能被 Java 调用,因为在底层上它并不是静态方法。如果想把它变成真正的 Java 静态方法,需要使用 @JvmStatic 注解:

class PremadeWorldMap {
    companion object {
        private const val MAPS_FILEPATH = "nyethack.maps"
        @JvmStatic
        fun load() = File(MAPS_FILEPATH).readBytes()
    }
}

解构声明

一个普通类实现了 componentN 函数,并用 operator 修饰,就可以用于解构赋值。

class Coordinate(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}
val (x, y) = Coordinate(1, 2)
println(x) // -> 1

数据类自动实现了这些 componentN 函数。

data class Coordinate(val x: Int, val y: Int)
val (x, y) = Coordinate(1, 2)
println(x) // -> 1

运算符重载

Kotlin 支持对 ++===>[] 等运算符进行重载,方式是在类的内部定义这些运算符对应的函数,并用 operator 来修饰。

data class Coordinate(val x: Int, val y: Int) {
    operator fun plus(other: Coordinate) = Coordinate(x + other.x, y + other.y)
}

enum class Direction(private val coordinate: Coordinate) {
    NORTH(Coordinate(0, -1)),
    EAST(Coordinate(1, 0)),
    SOUTH(Coordinate(0, 1)),
    WEST(Coordinate(-1, 0));

    fun updateCoordinate(playerCoordinate: Coordinate) =
        coordinate + playerCoordinate
}

上面的例子用 operatorCoordinate 这个类定义了一个 plus() 函数,于是两个 Coordinate 对象就可以用 + 相加了。

运算符 对应的函数
+ plus
+= plusAssign
== equals
> compareTo
[] get
.. rangeTo
in contains

注意,像 Java, Ruby 等编程语言一样,在重写了 equals() 函数的情况下,通常要同时重写 hashCode() 函数。这是一个普遍存在的契约。

open class Weapon(val name:String, val type: String) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Weapon

        if (name != other.name) return false
        if (type != other.type) return false

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + type.hashCode()
        return result
    }
}

另外,这里 equals()hashCode() 看似没有使用 operator 修饰,但实际上是因为在 Any 类里它已经被声明为 operator 了,因此这里 override 可以省略掉 operator

泛型

泛型允许你在定义时使用占位符来表示类型(例如 T),实际使用时再指定具体的类型。这种机制避免了使用强制类型转换,提高了代码的灵活性和可读性。

class Box<T> {
    private var value: T? = null

    fun putIn(value: T) {
        this.value = value
    }

    fun takeOut(): T? {
        return value
    }
}

fun main() {
    val stringBox = Box<String>()
    stringBox.putIn("Hello, 泛型!")
    println(stringBox.takeOut()) // 输出: Hello, 泛型!

    val intBox = Box<Int>()
    intBox.putIn(123)
    println(intBox.takeOut()) // 输出: 123
}

没有泛型的情况下,通常使用顶层的父类(例如 Kotlin 中的 Any 或者 Java 中的 Object)配合强制类型转换来实现类似的功能。

泛型类

泛型可以定义在类上,定义了泛型参数的类,叫作泛型类。泛型类的泛型声明在类名的后面。泛型通常用一对尖括号加上泛型名称来声明,例如 <T>

在泛型类上定义了的泛型,可以在该类的内部使用:

class LootBox<T>(item: T) {  
    private var loot: T = item  
}

泛型函数

泛型还可以定义在函数上,定义了泛型的函数,叫作泛型函数。泛型函数的泛型声明在函数名前面。

在泛型函数上定义的泛型只能在该函数内使用:

class LootBox<T>(item: T) {  
    var open = false  
    private var loot: T = item  
  
    fun fetch(): T? {  
        return loot.takeIf { open }  
    }  
      
    fun <R> fetch(lootModFunction: (T) -> R): R? {  
        return lootModFunction(loot).takeIf { open }  
    }  
}

泛型约束

前面示例中的泛型都没有被约束,因此传递什么类型进去都可以。也因此,LootBox<T> 不知道传入的 T 到底是什么类型,就无法访问其属性和函数。

class LootBox<T>(item: T) {  
    private var loot: T = item  

    fun show() {  
        println("Loot value is ${loot.value}") // 无法访问 value 属性,无法编译
    }  
}

可以用一个父类或者接口来约束泛型的类型:

open class Loot(val value: Int)  
  
class Fedora(val name: String, value: Int) : Loot(value)  
  
class Coin(value: Int) : Loot(value)  
  
class LootBox<T : Loot>(item: T) {   
    private var loot: T = item  
  
    fun show() {  
        println("Loot value is ${loot.value}")  
    }  
}

这样编译器就可以知道传入的是 Loot 类型,就可以访问它的属性和函数了。

多个类型约束

如果针对某个泛型有多个类型的约束,需要用到 where 关键字。

open class Fruit(val weight: Double)
interface Ground{}

class Watermelon(weight: Double): Fruit(weight), Ground
class Apple(weight: Double): Fruit(weight)

fun <T> cut(t: T) where T: Fruit, T: Ground {
    print("You can cut me.")
}

cut(Watermelon(3.0)) //允许
cut(Apple(2.0)) //不允许

类型擦除

Java 的 1.5 版本以前不存在泛型,容器类通常是用 Object 来存放各类对象。出现泛型之后,为了兼容旧版本的代码,只在编译时检查类型。在编译之后,会丢掉类型信息,最终保存的仍然是 Object

而类型检查是在编译时做的事。自动类型转换也可以靠编译器解决:

// 源代码
ArrayList<String> list = new ArrayList<>();
list.add("hello");
String str = list.get(0);

// 编译后等价于
ArrayList list = new ArrayList();
list.add((Object)"hello");  // 这里其实不需要显式转换,因为String本来就是Object
String str = (String)list.get(0);  // 这里编译器插入了转换

因为类型擦除的存在,ArrayList<Apple> 在运行时并不知道自己存的是 Apple,它只知道里面是一个 Object

Kotlin 的泛型机制与 Java 一样,因此也继承了 Java 的类型擦除的特性。需要在运行时获知泛型类型的时候,要用到一些技巧,例如匿名内部类,或者内联函数。

获取运行时类型

通常情况下 Kotlin 不允许对泛型参数做类型检查。

fun <T> randomOrBackupLoot(backupLoot: () -> T): T {  
    val items = listOf(Coin(14), Fedora("a fedora of the ages", 150))  
    val randomLoot: Loot = items.shuffled().first()  
    return if (randomLoot is T) { // 无法编译
        randomLoot  
    } else {  
        backupLoot()  
    }  
}

以上代码会无法编译。但 Kotlin 提供了关键字 reified,允许你在运行时保留类型信息。

inline fun <reified T> randomOrBackupLoot(backupLoot: () -> T): T {  
    val items = listOf(Coin(14), Fedora("a fedora of the ages", 150))  
    val randomLoot: Loot = items.shuffled().first()  
    return if (randomLoot is T) {  
        randomLoot  
    } else {  
        backupLoot()  
    }  
}

这样就可以正常编译并获取到类型信息了。但这么做有个前提条件,就是把函数用 inline 声明为内联函数。

泛型不变

泛型不变,指的是在泛型类型系统中,类型参数之间的关系不会自动继承或转换。具体来说,如果 TypeATypeB 的子类型,Generic<TypeA>Generic<TypeB> 之间没有继承关系。

这里的“不变”,指的是 “Generic<TypeA>Generic<TypeB> 之间没有继承关系” 这一点,不随着 TypeATypeB 的继承关系的变化而变化。

因此,以下代码无法编译,因为 Barrel<Loot> 并不是 Barrel<Fedora> 的父类。

open class Loot(val value: Int)

class Fedora(val name: String, value: Int): Loot(value)
class Coin(value: Int): Loot(value)

class Barrel<T>(var item: T)

fun main() {
    var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))

    lootBarrel = fedoraBarrel
    lootBarrel.item = Coin(15)
    val myFedora: Fedora = fedoraBarrel.item
}

假如这段代码可以编译,这里的 item 是可写的,我们可以把一个 Coin 赋值给它,结果造成实际上是 Barrel<Fedora> 实例的 fedoraBarrel,它的 item 变成了一个 Coin。因此,把 Barrel<Fedora> 的实例赋值给 Barrel<Loot> 变量是不允许的。所以 Kotlin 通过限制泛型类(Barrel<Loot>Barrel<Fedora>)之间的继承关系,来达到保证类型安全的目的。

协变

泛型引入了者一个“生产者”的概念。使用 out 关键字修饰泛型参数,可以把泛型类变成生产者:泛型参数只许使用,不许改变。

class Barrel<out T>(val item: T) // 因为不可写的特性,需要把 var 换成 val.

fun main() {
    var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))
    // 因为加了 out, 之后 lootBarrel 在使用时会变成 Barrel<Fedora>
    lootBarrel = fedoraBarrel 
    // lootBarrel.item = Coin(15)
    val myFedora: Fedora = lootBarrel.item
}

这种情况下,因为确定了 item 不能被重新赋值,已经保证了类型安全,所以泛型类 Barrel 的类型参数 T 可以设计成 out。或者换过来说,因为 Barrel 的类型参数 T 被设计成了 out,所以 itemBarrel 内只能读取,不能修改,所以不能再用 var 声明。

甚至 Barrel 不能拥有任何带有 T 参数的函数:

class Barrel<out T>(val item: T) {
  fun passItemIn(item: T) { // 无法编译
  }
}

这样的代码无法编译。所谓 “生产者”说白了,就是这个泛型类不能带有 T 类型作为参数的函数,但是可以带有返回 T 类型值的函数。

这样一来,泛型类 Barrel<Loot>Barrel<Fedora> 之间的继承关系就会跟着类型实参 LootFedora 的继承关系变化:如果 LootFedora 之间没有任何继承关系,那么 Barrel<Loot>Barrel<Fedora> 之间也没有任何继承关系;如果 LootFedora 的父类,那么 Barrel<Loot> 也是 Barrel<Fedora> 的父类。这时候,我们说 Barrel协变 的。

在 Kotlin 中,List 就是这样的一个泛型类。

逆变

反过来,泛型还有一个“消费者”的概念,使用 in 关键字来修饰泛型参数。这种情况下,泛型参数只能在泛型类内部使用,但外部无法访问。

class Barrel<in T>(item: T) // 因为不可读的特性,需要把 val 去掉。

fun main() {
    var fedoraBarrel: Barrel<Fedora> = Barrel(Fedora("a generic-looking fedora", 15))
    var lootBarrel: Barrel<Loot> = Barrel(Coin(15))
    fedoraBarrel = lootBarrel
}

这种情况下,item 可以写入,但无法取出,所以不能用 val 声明,更不能用 var 声明。当然,非要声明为成员变量的话,得用 private 修饰。有了 private 保证无法读取,用 var 还是 val 都可以了:

class Barrel<in T>(private var item: T)

同样,作为“消费者”,此时 Barrel 类中不能含有任何返回 T 的成员函数,但可以拥有带 T 参数的函数。

这时候泛型类 Barrel<Loot>Barrel<Fedora> 之间的继承关系就会跟着类型实参 LootFedora 的继承关系反向变化:如果 LootFedora 之间没有任何继承关系,那么 Barrel<Loot>Barrel<Fedora> 之间也没有任何继承关系;如果 LootFedora 的父类,那么 Barrel<Loot>Barrel<Fedora> 的子类。这时候,我们说 Barrel逆变 的。

逆变适用的场景特点是:如果一个类能处理父类型,那它一定也能处理子类型。例如一个人能照料所有动物,那么他一定能照料猫,此时一个 AnimalCarer<Animal> 可以安全地赋值给 AnimalCarer<Cat>。逆变可能比较反直觉,这里虽然 AnimalCat 的父类,但 AnimalCarer<Animal> 并不是 AnimalCarer<Cat> 的父类,反而是它的子类。

委托

属性委托

属性委托通过 by 关键字,将一个变量的 getter 和 setter 委托给一个实现了 ReadOnlyProperty 或 ReadWriteProperty 接口的对象。对于 val 声明的只读变量,需要实现 ReadOnlyProperty 接口,var 声明的变量则需要实现 ReadWriteProperty 接口。

ReadWriteProperty 接口有 getValue()setValue() 两个函数需要实现,而 ReadOnlyProperty 只有 getValue() 一个函数需要实现。

class Example {  
    var p: String by Delegate("example")  
}  
  
class Delegate(private var value: String) : ReadWriteProperty<Any?, String> {  
    override fun getValue(thisRef: Any?, property: KProperty<*>): String {  
        return "$thisRef, thank you for delegating '${property.name}' to me: $value"  
    }  
  
    override fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {  
        println("$value has been assigned to '${property.name}' in $thisRef.")  
        value = newValue  
    }  
}  
  
fun main(args: Array<String>) {  
    val e = Example()  
    println(e.p)  
  
    e.p = "another string"  
    println(e.p)  
}

此时,Examplep 属性的读写就被 Delegate 代理了。

也可以直接把一个变量的读写委托给一个类:

fun main(args: Array<String>) {  
    var x: String by Delegate("hello")
}

也可以不实现接口

委托类也可以不实现接口,直接提供 setValue()getValue() 两个函数,但要用 operator 来修饰,实现接口时能省略掉 operator 是因为 ReadOnlyProperty 和 ReadWriteProperty 那两个接口里声明的就是 operator

class Delegate(private var value: String) {  
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
    }
} 

内置委托

lazy()

lazy() 函数会返回一个委托,这个委托的取值代码只会在第一次尝试取值时执行,并将结果缓存起来,以供后续取值时直接使用。

fun main(args: Array<String>) {  
    val message by lazy {  
        println("取值代码执行中")  
        "hello"  
    }  
  
    println(message)  
    println(message)  
}

以上代码访问了 message 两次,但只会打印一次“取值代码执行中”。

lazy 委托是只读的,没有实现 setValue() 函数,因此 lazy 委托的变量只能用 val 声明。

线程安全

lazy 默认是线程安全的。该函数接受的第一个参数是个 LazyThreadSafetyMode,默认值是 LazyThreadSafetyMode.SYNCHRONIZED。使用锁确保初始化只会执行一次,线程安全,但有同步开销。

// 这两种写法是等价的
val name1: String by lazy { "John" }
val name2: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { "John" }

另外可选的值有:

  • PUBLICATION,使用它时,多个线程可能会执行多次初始化,但只有第一个初始化的结果会被使用,其他初始化结果会被丢弃。
  • NONE,使用它时没有任何线程安全保证,性能最好,适用于单线程环境。

Delegates.observable()

Delegates.observable() 是观察者模式的一种实现,用于观察属性值的变化。

class User {
    // 监控属性变化并通知 UI
    var name: String by Delegates.observable("") { _, old, new ->
        println("Name changed: $old -> $new")
        updateUI()
    }

    // 监控多个属性变化
    var age: Int by Delegates.observable(0) { _, old, new ->
        println("Age changed: $old -> $new")
        validateAge(new)
    }

    private fun updateUI() { }
    private fun validateAge(age: Int) { }
}

// 使用
fun main() {
    val user = User()
    user.name = "John"  // 输出: Name changed:  -> John
    user.age = 25      // 输出: Age changed: 0 -> 25

    println(user.name)  // 输出 John
}

Delegates.notNull()

Delegates.notNull() 类似 lateinit,用于声明延迟初始化某个变量。区别在于 Delegates.notNull() 支持基本类型,但不支持属性初始化检查(isInitialized),且会带来轻微的运行时开销。

fun main() {  
    var age: Int by Delegates.notNull()  
    println(age)  // age 未初始化,直接访问会抛出异常
}

Delegates.vetoable()

Delegates.vetoable() 是一个属性委托,允许在属性值改变之前进行拦截和验证。"veto" 的意思是"否决",这个委托可以否决属性值的改变。它接受两个参数,第一个参数是初始值,第二个参数是个 lambda,当该 lambda 返回 true 时,传入的值被接受,否则,传入的值被拒绝,修改失败。可以用来做验证器的功能。

class User(private val _name: String) {  
    var name: String by Delegates.vetoable(_name) { _, _, newValue ->  
        newValue.isNotBlank()  // 不允许空白名称  
    }  
  
    var age: Int by Delegates.vetoable(0) { _, _, newValue ->  
        newValue in 0..150  // 年龄必须在有效范围内  
    }  
  
    var email: String by Delegates.vetoable("") { _, _, newValue ->  
        newValue.contains("@")  // 简单的邮箱格式验证  
    }  
}  
  
fun main() {  
    val user = User("Tiago")  
    println(user.name)  
    user.name = "  "  
    println(user.name)  // 仍然是 "Tiago"
}

接口委托

接口委托同样用的是 by 关键字,但不同的地方在于,这个 by 的位置不是在变量的后面,而是在类的后面。你可以通过接口委托,把类的某个接口的实现,委托给它的某个构造参数。

有如下代码

class A {
    fun fun1() {
        println("fun 1 in A")
    }
}

class B {
    val a = A()
}

如果你想在类 B 里实现一个方法 fun1() 交给 a 代理, 最简单的办法是:

class B {
    val a = A()
    fun fun1() {
        a.fun1()
    }
}

但是需要代理的方法有很多,会出现许多重复的样板代码。

class B {
    val a = A()
    fun fun1() {
        a.fun1()
    }
    
    fun fun2() {
        a.fun2()
    }

    fun fun3()
	  ......
}

此时接口委托就派上用场了。首先你得声明一个接口,这个接口中要定义好所有你想代理的方法,同时让 AB 都实现该接口。因为 B 需要 A 代理这些方法,所以 AB 都有同样的方法,所以它俩实现同一个接口挺合理:

interface Fun1 {
    fun fun1()
}

class A : Fun1 {
    override fun fun1() {
        println("fun 1 in A")
    }
}

class B : Fun1 {
    val a = A()
    override fun fun1() {
        //...
    }
}

然后把 B 的方法委托给 a, 需要把成员变量 a 定义在构造方法的参数里,否则访问不到 a

class B(val a: A) : Fun1 by a

当然,a 也可以不是成员变量:

class B(a: A) : Fun1 by a

这样就完成了。同样,属性也可以被代理:

interface PageMeta {
    val totalPages: Int
    val currentPage: Int
}

data class BasePageMeta(
    val count: Int,
    val previousPage: Int,
    val nextPage: Int,
    override val totalPages: Int,
    override val currentPage: Int
) : PageMeta

data class PagedStuffListResponse(
    val meta: BasePageMeta,
    // ...
) : PageMeta by meta

还可以委托多个接口:

interface Printer { fun print() }
interface Speaker { fun speak() }

class Derived(p: Printer, s: Speaker) : Printer by p, Speaker by s

协程

Kotlin 协程