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 没有基本类型。但 Int
,Boolean
,Long
,Double
,Float
,Short
,Byte
,Char
这些类型,在 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")
}
同时这个例子展示了 else
在 when
表达式中的用法。
分支中含有多行代码时
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!~**`() 调用该函数
通常情况下,不要使用这种方式定义函数。这种方式常用于:
- 与 Java 互操作,例如
is
在 Kotlin 中是个关键字,在 Java 中不是,所以 Java 中可能会定义一个is()
方法,在 Kotlin 当中调用 Java 的is
方法就可以使用`is()`
。
fun doStuff() {
`is`() // Invokes the Java `is` method from Kotlin
}
- 另外,反引号函数可以用来写测试。
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()
函数。一个比较大的区别是,前者可以用 break
和 continue
来控制循环的中断或跳过,后者无法使用这两个关键字。
数组
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>
。与 Set
和 List
一样,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 里追加键值对,支持单个 Pair
、List<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()
也可以通过 Iterable
的 asSequence()
函数创建 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
关键字,而是在类名后面加上括号来调用构造函数创建实例。
类属性
与普通变量的声明一样,属性也是通过 val
和 var
关键字来区别是否可变。但是属性必须有初始值。
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)
就隐含了构造函数。
构造函数参数可以同时声明为属性,只要在前面加上 val
或 var
。
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
定义。
数据类重写了 Any
的 equals()
函数,只要值相同,==
比较都返回 true
。数据类的 toString()
和 hashCode()
也重写过。另外提供了一个 copy()
函数,用来创建内容完全一样的新实例。
data class Coordinate(val x: Int, val y: Int)
一个类要成为数据类,要符合一定条件。总结下来,主要有三个方面:
- 数据类必须有至少带一个参数的主构造函数;
- 数据类主构造函数的参数必须是
val
或var
; - 数据类不能使用
abstract
、open
、sealed
和inner
修饰符。
枚举类
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
修饰。
覆盖父类的属性
属性的覆盖和函数一样,需要 open
和 override
关键字
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()
传入 Player
和 Room
以外的类型,会在运行时抛出 ClassCastException
。
智能类型转换
同时,该示例中存在着隐式的自动类型转换,在条件表达式的第一个分支中,经过 is
的判断,any
是 Player
类型,因此在该分支的代码块中可以直接访问 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
}
上面的例子用 operator
给 Coordinate
这个类定义了一个 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
声明为内联函数。
泛型不变
泛型不变,指的是在泛型类型系统中,类型参数之间的关系不会自动继承或转换。具体来说,如果 TypeA
是 TypeB
的子类型,Generic<TypeA>
和 Generic<TypeB>
之间没有继承关系。
这里的“不变”,指的是 “Generic<TypeA>
与 Generic<TypeB>
之间没有继承关系” 这一点,不随着 TypeA
与 TypeB
的继承关系的变化而变化。
因此,以下代码无法编译,因为 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
,所以 item
在 Barrel
内只能读取,不能修改,所以不能再用 var
声明。
甚至 Barrel
不能拥有任何带有 T
参数的函数:
class Barrel<out T>(val item: T) {
fun passItemIn(item: T) { // 无法编译
}
}
这样的代码无法编译。所谓 “生产者”说白了,就是这个泛型类不能带有 T
类型作为参数的函数,但是可以带有返回 T
类型值的函数。
这样一来,泛型类 Barrel<Loot>
和 Barrel<Fedora>
之间的继承关系就会跟着类型实参 Loot
和 Fedora
的继承关系变化:如果 Loot
与 Fedora
之间没有任何继承关系,那么 Barrel<Loot>
和 Barrel<Fedora>
之间也没有任何继承关系;如果 Loot
是 Fedora
的父类,那么 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>
之间的继承关系就会跟着类型实参 Loot
和 Fedora
的继承关系反向变化:如果 Loot
与 Fedora
之间没有任何继承关系,那么 Barrel<Loot>
和 Barrel<Fedora>
之间也没有任何继承关系;如果 Loot
是 Fedora
的父类,那么 Barrel<Loot>
是 Barrel<Fedora>
的子类。这时候,我们说 Barrel
是 逆变 的。
逆变适用的场景特点是:如果一个类能处理父类型,那它一定也能处理子类型。例如一个人能照料所有动物,那么他一定能照料猫,此时一个 AnimalCarer<Animal>
可以安全地赋值给 AnimalCarer<Cat>
。逆变可能比较反直觉,这里虽然 Animal
是 Cat
的父类,但 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)
}
此时,Example
的 p
属性的读写就被 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()
......
}
此时接口委托就派上用场了。首先你得声明一个接口,这个接口中要定义好所有你想代理的方法,同时让 A
和 B
都实现该接口。因为 B
需要 A
代理这些方法,所以 A
和 B
都有同样的方法,所以它俩实现同一个接口挺合理:
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