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 协程

所有评论 0
目录
  1. 类型推断
  2. 编译时常量
  3. Kotlin 到底有没有基本数据类型?
  4. if / else
  5. 三元运算
  6. in, !in
  7. when
    1. 基本用法
    2. 多个分支同时匹配一个代码块
    3. 类型检查
    4. 跟 in,!in 一起使用
    5. 分支中含有多行代码时
    6. 不带参数的 when
  8. 可见性修饰符
    1. internal
  9. 参数
    1. 具名参数
    2. 默认值参数
    3. 可变参数
  10. 单表达式函数
  11. Unit 类型
  12. 返回值的类型推断
  13. Nothing 类型
  14. 函数重载
  15. 反引号函数
  16. main 函数
  17. 匿名函数
    1. lambda
    2. 函数类型
    3. lambda 的参数
    4. it 关键字
    5. 把 lambda 当作参数
    6. 函数内联
  18. 函数引用
  19. 返回函数
  20. 可空类型
  21. 安全调用
  22. ?. 操作符
  23. 使用带 let 的安全调用
  24. !!. 操作符
  25. ?: 操作符
  26. 抛出异常
  27. 捕获/处理异常
  28. Unchecked Exception
  29. 自定义异常
  30. 字符串模板
  31. apply()
  32. let()
  33. run()
  34. with()
  35. also()
  36. takeIf()
  37. takeUnless()
  38. 创建不可变列表
  39. 创建可变列表
  40. 获取列表元素
    1. 下标访问
    2. getOrElse() / getOrNull()
    3. first() / last()
    4. 解构赋值
  41. 遍历
  42. 创建 Map
  43. 取值
  44. 修改
  45. 序列 (Sequence)
  46. 创建 Sequence
    1. sequenceOf()
    2. asSequence()
    3. generateSequence()
    4. sequence()
  47. 操作 Sequence
    1. 中间操作和终结操作
  48. 创建类与实例化
  49. 类属性
    1. 属性的声明
    2. field, getter 和 setter
    3. 属性的可见性
  50. 计算属性
  51. 防范竞态条件(race condition)
  52. 初始化
    1. 主构造函数
    2. init 块
    3. 次构造函数
    4. 延迟初始化
    5. 惰性初始化
  53. 嵌套的类
  54. 数据类
  55. 枚举类
  56. 密封类
  57. 覆盖父类的函数
  58. 覆盖父类的属性
  59. 抽象类
  60. 类型检查
  61. 类型转换
    1. 智能类型转换
  62. Any
  63. 接口的属性声明
  64. 与抽象类的区别
  65. 函数名冲突
  66. 函数式接口(SAM)
  67. 对象声明(单例类)
  68. 对象表达式(匿名内部类)
  69. 伴生对象
  70. 解构声明
  71. 运算符重载
  72. 泛型类
  73. 泛型函数
  74. 泛型约束
    1. 多个类型约束
  75. 类型擦除
    1. 获取运行时类型
  76. 泛型不变
    1. 协变
    2. 逆变
  77. 属性委托
    1. 也可以不实现接口
    2. 内置委托
  78. 接口委托
准则 博客 联系 反馈 © 2025 Geeknote