Kotlin

JetBrains主导开发的在JVM上运行的编程语言.
在2017年成为Android的官方编程语言, Kotlin目前在Android上的地位一定程度上源自于Android只支持非常落后的Java版本的事实.
Kotlin程序扩展名为 .kt.
Kotlin脚本扩展名为 .kts.
  • 与Java互操作的目标在一定程度上拖累了语言本身的设计,
    导致Kotlin比起一个新的语言更像是语法糖合集.
    随着Java本身的发展, Kotlin可能成为Java版本的CoffeeScript,
    最后被拥有新特性的Java杀死, 尽管目前这一征兆还不明显.
  • 受JVM的功能限制.
  • 位运算没有自己的运算符.
  • 和Java一样喜欢制造关键字.
  • 与内置DSL相关的所有函数和语法都蠢透了.
  • 采用与类C语言相反的类型后置于标识符的写法.
  • 类型推导
  • 函数是一等公民
  • 与Java生态高度兼容, 可以直接互操作, 在一个项目中无缝混用Java和Kotlin.
  • 具名传参
  • 可选参数, 参数默认值
  • 可作为脚本运行, 不需要定义main函数
  • 不强制要求捕获异常
  • REPL
  • 分号可选
  • 函数重载
  • 基于类型系统的null值安全
  • 解构赋值
  • 倾向于表达式而不是语句.
    Kotlin类似Rust, 大量的语法都是表达式, 表达式中的最后一个命令会成为表达式的结果.
  • 中缀函数
  • 多范式编程
  • 扩展函数/属性, 可以直接给已经存在的类添加方法和属性
  • 运算符重载
  • 协程
  • Actor模型
  • Kotlin从Scala语言上借鉴了很多, 但更像是精挑细选有利特性得到的子集,
    这使得它比Scala适合用于工程, 因为范式选择的余地更小, 学习成本更低.
  • Kotlin兼容Java生态的能力比Scala强.
  • Kotlin的编译速度比Scala快.
  • Kotlin社区没有Scala社区那么有毒.
JetBrains在2020年发布的语言插件, 可帮助将Kotlin运用在Android以外的地方, 例如iOS.
尽管Multiplatform项目整体处于Alpha阶段, 但推动这个项目的组织显然缺乏务实的远见,
因为其中的很多项目要么明显是实验性的, 要么是纯粹浪费人力的笑话.
用于iOS和Android的单一代码库.
typealias OscarWinners = Map<String, String>
Kotlin允许在不接触类的定义的情况下在外部扩展它, 不需要类有open修饰符.
该特性类似于Go语言的方法, 本质上是创建了一个以接收器(receiver, 类的实例)为首个参数的函数.
Kotlin的标准库普遍使用扩展功能.
扩展函数可以在顶级作用域和类中使用, 声明时应该尽可能缩小扩展函数的作用域, 以免意外引起问题.
// 扩展类的方法
fun String.addEnthusiasm(amount: Int = 1) = this + "!".repeat(amount)
println("Hello World".addEnthusiasm())
// 扩展类的伴生对象
fun String.Companion.toURL(link: String) = java.net.URL(link)
// 扩展匹配接口的任意函数
fun <T, R, U> ((T) -> R).andThen(next: (R) -> U): (T) -> U = { input: T ->
next(this(input))
}
扩展属性没有field数据, 因此没有默认的set和get方法.
val String.newVowels
get() = count { "aeiouy".contains(it) }
interface Clickable {
fun click()
fun showOff() = println("clickable") // 接口成员可以带有默认实现
}
class Button : Clickable {
override fun click() = println("clicked")
}
函数成接口允许使用者根据lambda表达式直接创建出一个符合接口的只有单一方法的object.
这种接口的存在说白了还是命令模式的遗毒, 因为绝大多数情况都可以直接用匿名函数或lambda表达式替代.
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
val isEven = IntPredicate { it %2 == 0 }
  • 接口不能包含字段.
  • 一个类只能继承一个抽象类, 但可以实现多个接口.
  • 抽象类有构造函数, 接口没有构造函数.
Kotlin里类似于静态类或类单例的一种结构, 除了没有构造函数以外, 跟类基本一致, 初始化通过init块完成.
object与类的一个不同之处是它还可以在内部内嵌类, 因此也可以将object作为命名空间使用.
object可以通过companion关键字内嵌在类里, 这被称为伴生对象.
每个类只能有一个伴生对象, 实际上就是给类加上静态方法.
class Person {
companion object {
fun load() {
// ...
}
}
}
class Class private constructor(val name: String) {
// ...
companion object {
fun create(name: String): Class {
val instance = Class(name)
// ...
return instance
}
}
}
匿名对象相当于Java里的匿名内部类.
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
// ...
}
}
)
Kotlin里类似于结构体的一种贫血数据容器, 自带equals, hashCode, toString方法的实现.
数据类不能被继承.
数据类独有的方法, 可以复制自身产生一个新的实例, 并在复制过程中改变属性的值, 很适合创造不可变量.
Kotlin里类似枚举的一种结构, 支持构造函数和方法.
enum class Direction {
NORTH
, EAST
, SOUTH
, WEST
}
// 构造函数和方法的使用场景
enum class Color(val rgb: Int) {
RED(0xFF0000)
, GREEN(0x00FF00)
, BLUE(0x0000FF)
, YELLOW(0xFFFF00); // 分号是必须的
fun containsRed() = (this.rgb and 0xFF0000 != 0)
}
不建议使用.
  • let
  • run
  • with
  • apply
  • also
密封类可防止该类在包外被继承.
密封类本身是抽象的, 不能被直接实例化, 必须由该包下的其他类继承后使用.
密封类的特性使得它非常适合作为一种"表示这些类同属于一个类别"的共同基类,
经常被用于配合when表达式模拟模式匹配.
Kotlin不需要类与文件同名, 也允许在一个文件里定义多个类.
Kotlin的类本身就是构造函数(称为主构造函数), 不需要new关键字.
个人认为Kotlin的类在写法上还不如Java原本的设计来得好, 至少后者不会把构造函数和类的成员定义混合在一起, 将构造函数拆成三个部分是极其荒谬的.
事实上, 在Kotlin里可以完全忽略主构造函数, 只使用次构造函数:
class Person(val firstName: String, val lastName: String) {}
// 等价于
class Person {
val firstName: String
val lastName: String
constructor(firstName: String, lastName: String) {
this.firstName = firstName
this.lastName = lastName
}
}
// 后者相比前者有很多优点:
// - 不会强制把构造函数的参数变成类的属性
// - 可以完成具有复杂逻辑的初始化
// - 重载起来更直观, 不用回去查看类的头部
// - 不会在类的首行塞入太多内容
Kotlin类按照以下顺序初始化:
  • 主构造函数的参数
  • 属性, 从上往下依序初始化.
    如果期间遇到init块, 则运行init块.
  • 次构造函数
初始化块是用来检查构造函数的参数使用的, 用来弥补主构造函数的不足(主构造函数不能运行复杂的初始化逻辑).
一个类可以有零个或多个init块.
由于init块的执行时机与它的位置有关, 应该将init块放在它使用到的属性的后面.
次构造函数用来弥补主构造函数的不足(主构造函数不能支持构造函数重载), 次构造函数不能定义新的属性.
一个类可以有多个次构造函数, 用来满足不同的重载.
每个类的可变属性具有field, getter, setter三个数据, field用来存储属性的值, 只在getter和setter内部可见.
不可变属性没有setter.
class Rect(val height: Int, val width: Int) {
// 定义类的不可变属性
val isSquare: Booolean
// 覆盖该属性的getter
get() = height == width
// 定义类的可变属性
var name: String
// 覆盖该属性的setter
set(value) {
field = value.trim()
}
}
这个修饰符允许类的属性延迟初始化的时间, 应该谨慎使用, 因为会关闭一些编译器检查.
声明在类中的类, 只能在类的实例中访问.
  • public(默认)
  • private
  • protected
  • internal
默认情况下, Kotlin类不允许被继承, 需要手动给父类添加open修饰符.
class Child(arg1, arg2) : Super(arg1, arg2)
当子类覆盖父类方法时, 要求在父类的方法前加上open修饰符, 在子类的方法前加上override修饰符.
class Point(val: x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
val name = "Name" // val(value), 只读变量, 不可重新赋值
var name = "Name" // var(variable), 可变变量, 可重新赋值
const val MAX_VALUE = 100 // 编译时常量, 只能在顶级作用域定义
Kotlin泛型的协变逆变详见 协变逆变.org.
class Box<T>(item: T) {
fun <R> map(fn: (T) -> R): R {
return fn(item)
}
}
// 有单个约束的泛型(T必须是Item类或Item的子类)
open class Item(val value: Int)
class Box<T : Item>(item: T) {
fun <R> map(fn: (T) -> R): R {
return fn(item)
}
}
// 有多个约束的泛型
fun <T> useAndCLose(input: T) where T: AutoClosable, T: Appendable {
// ...
}
Java里有基本类型和引用类型两种类型, 基本类型小写, 引用类型首字母大写.
基本类型的性能比引用类型的性能好, 当需要将基本类型当作引用类型使用时, 会发生自动装箱.
Kotlin没有基本类型, 全部类型都是首字母大写, 由编译器决定实际运行时使用的类型,
从而减少开发人员的选择.
根类型:
  • Any? 所有类型
  • Any 所有非空类型
整数:
  • Byte
  • Short
  • Int
  • Long
浮点数:
  • Float
  • Double
字符/字符串:
  • Char
  • String
专用于函数返回值的类型.
相当于其他语言的void, 即"无返回值".
相当于TypeScript的never, 表示该函数会抛出异常.
通过listOf创建, 列表是只读的.
通过mutableListOf创建.
val (a, b, c) = "1,2,3".split(',')
val (_, _, c) = "1,2,3".split(',')
// 解构赋值是以下代码的语法糖, component系列方法是编译器自动生成的
val a = obj.component1()
val b = obj.component2()
val n = obj.componentN()
通过setOf创建, 集合是只读的, 集合的项目是唯一的.
通过mutableSetOf创建.
// 创建一个Map<String, Double>
mapOf(
"Eli" to 10.5
, "Mordoc" to 8.0
, "Sophie" to 5.5
)
// 中缀函数to是Pair(二元组)的包装, 上方的代码等价于
mapOf(
Pair("Eli", 10.5)
, Pair("Mordoc", 8.0)
, Pair("Sophie", 5.5)
)
通过mutableMapOf创建.
原则上, 在Kotlin里应该使用List, 只在与Java交互时使用数组.
类型 创建函数
IntArray intArrayOf
DoubleArray doubleArrayOf
LongArray longArrayOf
ShortArray shortArrayOf
ByteArray byteArrayOf
FloatArray floatArrayOf
Array arrayOf
'a'..'z' // 从a到z的所有字符, 是'a'.rangeTo('z')的语法糖, 类型为CharRange
1..5 // 1, 2, 3, 4, 5, 是(1).rangeTo(5)的语法糖, 类型为IntRange
"hell".."help" // 两个字符串之间的所有字符串, 是"hell".rangeTo("help")的语法糖, 类型为ClosedRange<String>
// 用in关键字检查值是否属于某个区间
'a' in 'a'..'z'
'A' !in 'a'..'z'
1 in 1..5
0 !in 1..5
// 反向区间
5.downTo(1) // 5, 4, 3, 2, 1
// 通常会使用中缀调用的写法
5 downTo 1
// until
1 until 5 // 1, 2, 3, 4
// 带有步进的until, 实际上step是一个类似filter的函数
1 until 10 step 3 // 1, 4, 7
val names = listOf("Eli", "Mordoc", "Sophie")
// 通过for循环
for (name in names) {
println(name)
}
// 通过forEach函数
names.forEach { name ->
println(name)
}
// 带有index的forEach
names.forEachIndexed { index, name ->
println("$index: $name")
}
Kotlin标准库内置了几种常见的断言函数.
  • requireNotNull, 参数为null值时抛出IllegalArgumentException异常, 否则返回非null值
    它的抛出的异常类型决定了该断言通常写在函数开头, 用来检查函数的输入值.
  • checkNotNull, 参数为null值时抛出IllegalStateException异常, 否则返回非null值
    可以在任何地方使用, 和requireNotNull的唯一区别是抛出的异常类型不同.
  • error, 参需为null时抛出异常并打印错误信息, 否则返回非null值
  • require, 参数为false时抛出异常
  • assert, 参数为false是抛出异常, 并在编译器上标记
Kotlin的函数定义可以放在其他函数内部.
// 单表达式函数
fun greet(name: String = "World"): String = "Hello $name"
// 变长参数 vararg
fun listOf<T>(vararg values: T): List<T> {
// ...
}
listOf(*args) // 用*运算符展开一个集合
// 匿名函数(大部分情况下, 应该编写lambda表达式而不是匿名函数)
val greet = fun(name: String): String = "Hello $name"
// 中缀(infix)函数
infix fun Any.to(other: Any) = Pair(this, other)
// 一般调用
1.to("one")
// 中缀调用
1 to "one"
// 获取函数/方法的引用
::greet
Class::greet
如果函数定义前有inline修饰符, 则该函数的调用在编译时会被优化成内联于调用处的代码块, 这可以提升函数的执行速度.
用于修饰内联函数里的其他函数, 可取消该函数的内联行为.
当lambda表达式是函数调用的最后一个参数时, 可以不将lambda表达式写在括号内
(如果函数调用只有lambda表达式这一个参数, 括号可以省略).
// lambda表达式
val log: (String, String) -> String = { sender, message ->
// lambda表达式内不能使用return关键字, 只有最后一个表达式会返回
"$sender: $message"
}
// 有参数类型定义的lambda表达式
{ sender: String, message: String ->
"$sender: $message"
}
// 无参数lambda表达式可以省略参数部分
{
val message = "Hello World"
println(message)
}()
// 单参数lambda表达式可以用it关键字缩写
{ it == 'something' }
// 相当于
{ x -> x == 'something' }
Kotlin里的 if...else 语句和Rust一样是表达式.
val result = if (condition) {
"True"
} else {
"False"
}
// 省略花括号
val result = if (condition) "True" else "False"
相当于Kotlin里的switch.
val result = when (value) {
0 -> "It is small"
in 1..99 -> "It is normal"
else -> "It is big"
}
'a' // 字符
"Hello World" // 字符串
"""Hello World""" // raw字符串, 可跨行
val multiline = """Line1
|Line2
|Line3""" // 使用|创建不包含空白缩进的跨行字符串
val multiline = """Line1
~Line2
~Line3""".trimMargin("~") // 创建不包含空白缩进的跨行字符串的另一种方法
val name = "World"
println("Hello $name") // 插入变量
println("Hello ${name}") // 插入表达式
==
!=
=== // 引用相等
!== // 引用不等
is // 检查实例(左值)是否来自于类(右值)
as // 类型转换
as? // 安全类型转换, 会把ClassCastException转换为null
+ // 可用于字符串拼接
// 逻辑运算符
&&
||
!
?. // 安全调用操作符, 只在非null值时执行后续命令
?: // 空合并操作符(Elvis运算符), 左值为null时执行右侧的表达式, 相当于TypeScript的??
!! // 非空断言操作符, 强制Kotlin跳过编译时null值检查
Kotlin里的委托类本质上是这个东西:
// 这里不能用Manager继承Worker来实现, 因为这么做会破坏语义
class Manager(val worker: Worker) {
fun work() = worker.work()
fun takeVacation() = worker.work()
}
val pm = Manager(Programmer())
pm.work()
即用于调用另一个类的成员的机制, 作为"代理"的类的代码只是为了将调用"路由"到实际的目标上而已.
Kotlin创建了委托机制来简化这种手工代码的编写, 当应用在类上时, 被称为委托类:
// 语法要求by的左边是一个接口, 右边是接口的实现.
class Manager(val staff: Worker): Worker by staff {
override fun takeVacation() = staff.work() // 委托类中被定义的方法不会路由到staff
}
val pm = Manager(Programmer())
pm.work()
一个委托类可以包含多个委托, 发生成员冲突时, 需要手动编写相关方法来解决冲突.
委托还可以发生在属性上, 只要属性本身具有setValue和getValue操作符.
class Post(dataSource: MutableMap<String, Any>) {
val title: String by dataSource // 访问title时, 实际上会调用dataSource.getValue("title")或dataSource.setValue("title", value)
val likes: Int by dataSource // 访问likes时, 实际上会调用dataSource.getValue("likes")或dataSource.setValue("likes", value)
}
class Player() {
// 直到需要使用hometown的时候才会初始化
val hometown by lazy {
selectHometown()
}
}
import kotlin.properties.Delegates.observable
// observable的第一个参数是初始值
var count by observable(0) { property, oldValue, newValue ->
// ...
}
import kotlin.properties.Delegates.vetoable
// vetoable的第一个参数是初始值
var count by vetoable(0) { property, oldValue, newValue ->
newValue > oldValue // 仅当新值比旧值大时, 才允许赋值
}
https://kotlinlang.org/docs/coroutines-guide.html
Kotlin里关于协程的语法支持只有suspend修饰符一项, 其余的支持都是通过库函数完成的.
协程作用域是代码在协程世界里的运行环境, 它的 this 是一个CoroutineScope对象.
协程作用域内特有的一个属性, 该属性可被协程代码用于检查协程当前的状态.
如果isActive为false, 协程应该自主停止运行, 能够响应isActive变化的协程被称为协作式的(cooperative)协程.
协程调度器决定了协程被调度到哪里执行, 默认的协程调度器使用一个共享的后台线程池.
launch和async函数支持自定义协程调度器.
建立一个协程作用域, 运行代码, 并且阻塞直到内部的协程都运行完毕.
runBlocking是一个常规函数, 在非协程世界里运行, 用于连接非协程世界和协程世界.
建立一个协程作用域, 运行代码.
coroutineScope函数是非阻塞的.
coroutineScope函数是一个挂起函数, 在协程世界里运行.
启动一个协程, 返回该协程的Job对象.
launch函数是非阻塞的.
launch函数需要在协程作用域里调用.
协程中抛出的异常会传播.
该对象用于对协程进行细粒度的控制.
job.join() 阻塞式等待协程执行完毕.
job.cancel() 取消协程执行, 该方法要求协程是协作式的(cooperative), 非协作式的协程无法被取消.
协程被取消时, 协程里如果包含finally块, 则finally块里的代码仍然会被执行.
如果finally块里调用了挂起函数, 则会导致CancellationException, 该异常通常会被异常处理程序忽视.
在极少数情况下, 如果真的要在finally块里调用挂起函数, 可以通过 withContext(NonCancellable) 来绕过这个异常.
Kotlin的SupervisorJob可用于完成此目的, 即强制杀死非协作式的协程.
启动一个协程, 返回一个Deferred对象.
async函数是非阻塞的.
通过start参数(CoroutineStart.LAZY), async还允许先返回Deferred对象, 之后再手动启动协程.
在一个协程作用域里执行的多个async调用, 若其中一个协程抛出错误, 则其余协程全部会遭到取消.
协程中抛出的异常会被捕获, 传播到await调用的位置.
一个携带协程的结果的对象, 类似于Promise.
deferred.await() 阻塞式等待协程执行完毕并获取返回值.
启动一个带有timeout的协程, 当协程运行超出指定时间时, 会抛出TimeoutCancellationException.
另有withTimeoutOrNull, 将异常改为null.
启动一个协程, 返回一个ReceiveChannel对象, 详见Channel.
协程中抛出的异常会被捕获, 传播到receive调用的位置.
启动一个以接收消息并根据不同消息执行操作为目的协程,
之后可以通过send方法向该协程发送消息.
actor协程往往是高度内聚化的, 如果想要从中得到状态, 应该向其发送一个Deferred,
actor协程将通过完成此Deferred向外告知其瞬时的内部状态.
协程中抛出的异常会向外传播.
挂起函数是可能在运行中暂停, 让出计算资源的函数.
添加suspend修饰符就可以将一个函数变成挂起函数,
挂起函数的返回值与其作为普通函数时的返回值完全相同, 因此只需要最小的修改.
挂起函数也可以在非协程世界里调用, 这种情况下挂起函数会阻塞式运行.
调用它会导致协程被暂停特定时间.
select表达式与Go语言的select类似, 等待多个挂起函数(通常是Channel的onReceive), 选择第一个可用的挂起函数的返回值.
val channel = Channel<Int>() // Channel的第一个参数决定了缓冲大小, 默认为无缓冲
launch {
for (x in 1..5) {
channel.send(x * x)
}
}
repeat(5) {
println(channel.receive())
}
用Channel和produce函数很容易建立管道:
  • Channel对象本身是可迭代的.
  • produce函数执行lambda表达式并返回一个新的 ReceiveChannel.
runBlocking {
val numbers = produce<Int> {
for (x in 1..5) {
send(x * x)
}
}
val squares = produce<Int> {
for (x in numbers) {
send(x * x)
}
}
repeat(5) {
println(squares.receive())
}
}
未捕获的异常会传播到协程构建器调用时的CoroutineExceptionHandler类型的参数.
协程里出现异常时, 被捕获的会是第一个异常, 第一个异常之后的其他异常会被附加给第一个异常.