一、基础知识

属性

Kotlin 中的属性

Kotlin 类中的属性和 Java 类中的字段(成员变量)不同,Java 中的字段只是一个变量,而在 Kotlin 中,一个类的属性通常由 backing field + getter + setter 组成:

  • backing field:实际存在值的字段,相对于 Java 中的字段;

  • getter:访问属性时,实际调用的方法;

  • setter:赋值属性时,实际调用的方法。

因此,我们可以显式地给 Kotlin 中的属性设置 getter/ setter 方法,一个 var 变量默认具有的 getter/setter 如下,get()set() 中的 field 变量就是 backing field,表示实际存储值的变量。

class Demo {
    var x = 10
        get() {
            return field
        }
        set(value) {
            field = value
        }
}

var 和 val

在 Kotlin 中 val 修饰的变量并不是常量,而是“只读变量”。所谓只读,指的是它只能被赋值一次,但并不保证它的值在运行时不可变。

由上面我们知道,Kotlin 中的属性是由 backing field + getter + setter 组成的,但是 var 和 val 修饰的修饰的属性有所不同:

  • var:有 getter 和 setter。

  • val:只有 getter,没有 setter。

由于 val 修饰的属性不能设置 setter,因此我们只能进行一次初始化,后续无法对属性进行赋值。但是可以设置 setter,因此仍然可能在每次访问时返回不同的值。例如:

class Simple {
    val x: Int
        get() {
            return Random.nextInt(10)
        }
}

fun main() {
    val s = Simple()
    println(s.x) // 第一次调用
    println(s.x) // 第二次调用
}

运行结果中,s.x 的两次输出可能不同。这说明 val 并非真正的常量,只是外部无法直接对其重新赋值。

const

如果需要定义真正意义上的常量(类似 Java 中的 final static),就必须使用 const 关键字。
const 修饰的属性必须在 编译期 就确定值,因此它只能修饰 顶层属性object 单例对象中的属性,或 companion object 中的属性。

// 顶层属性
const val TAG = "666" 

// 在 object 中使用
object SimpleObj {
    const val X = 20
}

class Simple {
    // 在伴生 object 中使用
    companion object {
        const val NUM = 10
    }
}

object

Kotlin 中的 Object 是一个单例类,等价于 Java 中静态变量写法的单例:

public final class Singleton {
   @NotNull
   public static final Singleton INSTANCE = new Singleton();

   private Singleton() {
   }
}
object Singleton {

}

运算符重载

属性委托

什么是属性委托?

在 Kotlin 中,by 是一个关键字,专门用在委托 (Delegation) 语法里。它的作用是:将某个属性的 getset 操作交给另一个对象(委托对象)来完成,而不是由属性自己直接实现。

例如:

var text by someDelegate

Kotlin 在编译时会转换成:

var text: String
    get() = someDelegate.getValue(this, ::text)
    set(value) = someDelegate.setValue(this, ::text, value)

也就是说,只要一个类实现了 getValuesetValue 这两个运算符函数,它就能被用作属性的委托对象。

自定义委托

下面是一个自定义的属性委托类,每次在 getset 时输出日志,便于调试:

class LogDelegate<T>(private var value: T) {
    operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): T {
        println("Get ${property.name} = $value")
        return value
    }

    operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, newValue: T) {
        println("Set ${property.name} from $value to $newValue")
        value = newValue
    }
}


fun main() {
    var x: Int by LogDelegate(10)

    println(x)   // Get x = 10
    x = 20       // Set x from 10 to 20
    println(x)   // Get x = 20
}

lazy

Kotlin 标准库已经内置了一些常用的委托,不需要我们自己实现 getValue / setValue,直接拿来就能用。

lazy 委托会在属性第一次访问时才执行初始化逻辑,并缓存结果。常用于只需要初始化一次的场景,例如单例模式。

val name: String by lazy {
    println("init...")
    "Tony"
}

fun main() {
    println(name) // 第一次访问时执行 block
    println(name) // 第二次访问直接用缓存
}


二、扩展方法

什么是扩展方法?

Kotlin 的扩展方法(Extension Function)是一种在 不修改类源码、不继承该类 的情况下,为现有类“添加”新函数的机制。

fun String.lastChar(): Char {
    return this[this.length - 1]
}

fun main() {
    println("Kotlin".lastChar()) // 输出 'n'
}

需要注意的是,扩展方法 并没有真正修改类本身。它在 编译期 会被静态解析成一个 静态方法调用,调用对象(Receiver)作为第一个参数传入。所以扩展方法只是语法糖,不是真正的类方法。编译后的 Java 代码大致是:

public static final char lastChar(@NotNull String $this$lastChar) {
    Intrinsics.checkNotNullParameter($this$lastChar, "<this>");
    return $this$lastChar.charAt($this$lastChar.length() - 1);
}

扩展方法和类的成员方法同名怎么办?

如果扩展方法和类的成员方法 同名且签名相同,那么 成员方法优先

class Student() {
    fun study() {
        println("study...")
    }
}

// 扩展方法与成员方法签名相同,成员方法优先
fun Student.study() {
    println("sleep...")
}

// 签名不同,不会被屏蔽
fun Student.study(book: String) {
    println("study $book")
}

fun main() {
    val student = Student()
    student.study() // study...
    student.study("Android")    // study Android
}

父类的扩展方法子类可以调用吗?

可以使用。原因是扩展方法在本质上是一个静态函数,Receiver 作为第一个参数传入,因此子类对象同样可以传进去使用。

例如 Kotlin 为 CharSequence 定义了扩展方法 isEmpty(),而 String 作为子类自然也能调用:

public inline fun CharSequence.isEmpty(): Boolean = length == 0

fun main() {
    val str: String = ""
    println(str.isEmpty()) // true
}


三、高阶函数

什么是高阶函数?

高阶函数是指:可以接收函数作为参数,或者返回一个函数作为结果的函数。在 Kotlin 中,函数可以像对象一样传递、存储和返回。高阶函数的强大之处在于它们常常与 Lambda 表达式 一起使用,使得代码更加简洁。

常见场景:

  • 函数式编程风格:集合操作 list.map{ }

  • 回调函数(Callback):button.setOnClickListener { println("clicked") }

fun operate(x: Int, y: Int, op: (Int, Int) -> Int): Int {
    return op(x, y)
}

fun main() {
    val sum = operate(3, 5) { a, b -> a + b }
    println(sum) // 输出 8
}

在 JVM 上,Kotlin 的高阶函数其实是通过 函数类型对象 来实现的。

  • 在 Kotlin 中 (Int) -> String 这种函数类型,本质上是编译成 kotlin.jvm.functions.FunctionN 接口 的匿名类。

  • FunctionN 是一组接口,比如:

    • Function0<R>:无参函数

    • Function1<P1, R>:一个参数

    • Function2<P1, P2, R>:两个参数

    • 以此类推,最多支持 Function22

上面代码中的 opreate() 函数编译后的内容大致如下,函数参数 op 实际上是 Function2 接口的实例。

public static final int operate(int x, int y, @NotNull Function2 op) {
    Intrinsics.checkNotNullParameter(op, "op");
    return ((Number) op.invoke(x, y)).intValue();
}

Kotlin 的高阶函数是通过 函数类型 编译成 FunctionN 接口的实现来完成的,Lambda 会被编译成实现了 FunctionN 接口的匿名类对象。如果 Lambda 捕获了外部变量,就会生成一个闭包类。为了避免性能开销,Kotlin 提供了 inline,在编译期直接内联代码,从而减少对象分配和调用开销。

几个有用的高阶函数

Kotlin 官方内置了很多有用的高阶函数,下面是他们的源码:

// 执行一段映射,返回 R,T 作为参数传递至 block
public inline fun <T, R> T.let(block: (T) -> R): R

// 执行一段映射,返回 R, Receiver (this) 传递至 block
public inline fun <T, R> T.run(block: T.() -> R): R

// 执行一段逻辑,T 作为参数传递至 block
public inline fun <T> T.also(block: (T) -> Unit): T

// 执行一段逻辑,T 作为 Receiver (this) 传递至 block
public inline fun <T> T.apply(block: T.() -> Unit): T

// 重复执行 action 多次
public inline fun repeat(times: Int, action: (Int) -> Unit)

使用实例:

val s = "abc"

// s 作为 Lambda 的参数传递
val r1 = s.let { it ->
    println(it.length)
    it.substring(2)
}
 println("r1 = ${r1}")

// s 作为 this 传递,可以直接操作属性和方法
val r2 = s.run {
    println(this)
    println(length)
    substring(1)
}
println("r2 = ${r2}")

// s 作为参数传递,返回值为 Unit
s.also { it->
    println("also, ${it}, ${it.length}")
}

// s 作为 this 传递,返回值为 Unit
s.apply {
    println("apply, $length")
}

带接收者的高阶函数

在 Kotlin 中,函数的参数不仅可以是普通函数,还可以是“带接收者的函数类型”。这种特性让我们能写出类似 DSL(领域特定语言)的优雅代码。下面通过一个简单的例子来体会。

class Turtle {
    var x = 0
    fun forward(step: Int) {
        x += step
    }
}

fun turtle(block: Turtle.() -> Unit): Turtle {
    val instance = Turtle()
    // 下面三种写法本质相同:
    // block.invoke(instance) // 显式调用
    // block(instance)        // 函数调用方式
    instance.block()          // 扩展方法调用方式
    return instance
}

fun main() {
    val t = turtle {
        forward(10)
        forward(20)
    }
    println(t.x)    // 输出 30
}

函数 turtle() 中的参数 block: Turtle.() -> Unit 表示这是一个“带接收者的函数类型”。Turtle 是接收者类型,意味着在 block 中可以直接调用 Turtle 的方法和属性。

带 Receiver 的函数,本质上是将 Receiver 作为第一个参数的函数,调用有三种方式:

  • block.invoke(instance):最原始的写法,Receiver 作为函数的第一个参数;

  • block(instance):看起来像普通函数调用;

  • instance.block():更直观,仿佛 blockTurtle 的扩展方法。

main 中,调用 turtle { ... } 时,this 默认就是一个 Turtle 对象。因此在代码块中,可以直接写 forward(10),而无需写 t.forward(10),读起来就像在命令“小乌龟”前进一样。


四、内联函数

在 Kotlin 中,可以使用 inline 关键字将函数声明为内联函数。所谓内联,是指在 编译期 编译器会进行“代码展开”,具体包括两方面:

Kotlin 中使用 inline 关键字修饰的函数是内联函数。在编译时会将:

  • 函数本身的调用会被内联到调用处,避免函数调用的栈开销;

  • 函数参数(尤其是 Lambda 表达式)也会被内联到调用处,避免生成额外对象,从而提升性能。

例如,Kotlin 标准库中的 forEach 就是一个典型的内联函数:

public inline fun IntArray.forEach(action: (Int) -> Unit): Unit {
    for (element in this) action(element)
}

当调用 forEach 时,编译器会直接将传入的 Lambda 代码“拷贝”到循环体中,而不是生成额外的方法调用。因此,内联在高阶函数(接收 Lambda 参数的函数)中尤为常用。

内联高阶函数的 return

在内联高阶函数中,return 的行为有一些特殊之处。比如在 forEach 中,无法实现 break 的效果(直接跳出整个循环),只能实现 continue 的效果(跳过当前元素,继续下一次循环)。

val a = intArrayOf(1, 2, 3, 4)
a.forEach {
    if (it == 3) {
        return@forEach  // 局部返回,相当于 continue
    }
    println(it)
}

等价于传统的循环写法:

for (element in a){
    if (element == 3){
        continue
    }
    println(element)
}

这里的 return@forEach 被称为 标签返回(labeled return),它只跳过当前这次 Lambda 的执行,而不会终止整个外层函数。


五、泛型

六、反射

七、注解


八、协程基础

创建协程

先来看一个简单的示例代码:

fun main() {
    /**
     * 创建协程
     * 一个 suspend Lambda 可以通过 createCoroutine 方法创建一个协程
     */
    val continuation = suspend {
        println("suspend 匿名函数")
        fun1()
        5
    }.createCoroutine(object : Continuation<Int> {
        override val context: CoroutineContext = EmptyCoroutineContext

        override fun resumeWith(result: Result<Int>) {
            println("协程执行完毕,result = $result")
        }
    })

    /**
     * 启动协程
     */
    continuation.resume(Unit)
}

suspend fun fun1() = suspendCoroutine<Unit> {
    thread {
        Thread.sleep(1000)
        println("suspend fun1")
        it.resume(Unit)
    }
}

在这个例子中,我们通过 createCoroutine 创建了一个协程。它接收一个 Continuation<T> 参数,用来接收协程执行完毕的结果,并返回一个 Continuation<Unit>,我们可以通过调用它的 resume(Unit) 来启动协程。

源码中 createCoroutine 的定义如下:

public fun <T> (suspend () -> T).createCoroutine(
    completion: Continuation<T>
): Continuation<Unit>

可以看到,这个函数是 suspend function 的扩展函数,作用就是将 suspend { ... } 这样的协程体编译成协程框架可识别的对象。

挂起与恢复

我们通过一个 suspend Lambda 去创建一个协程,这个 suspend Lambda 就是协程具体执行的逻辑,或者称之为协程体

suspend { ... } 其实会被编译成一个 SuspendLambda 的实现类(位于 kotlin.coroutines.jvm.internal 包),它继承自 ContinuationImpl(基类是 BaseContinuationImpl),并实现了核心的 invokeSuspend() 方法。

这个方法就是协程的“状态机”,它会根据 label 字段决定从哪一步继续执行:

int label;

public final Object invokeSuspend(Object $result) {
    Object suspended = IntrinsicsKt.getCOROUTINE_SUSPENDED();
    switch (this.label) {
       case 0:
          ResultKt.throwOnFailure($result);
          System.out.println("suspend 匿名函数");
          Continuation cont = (Continuation)this;
          this.label = 1;
          if (StartCoroutineKt.fun1(cont) == suspended) {
             return suspended; // 挂起
          }
          break;
       case 1:
          ResultKt.throwOnFailure($result);
          break;
       default:
          throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
    }
    return Boxing.boxInt(5);
}

可以看到:

  1. label = 0:表示初次进入协程,执行第一段逻辑,遇到 fun1() 时可能挂起。

  2. label = 1:表示从 fun1() 恢复后继续往下执行。

  3. 每一个 suspend 调用点,都会编译成一个 case,挂起点就是 switch 状态机的跳转点。

为什么能恢复?

因为挂起函数本质上接收一个 Continuation 参数,当 fun1 内部调用 it.resume(Unit) 时,会触发外部 ContinuationresumeWith,重新进入 invokeSuspend(),并根据 label 跳到上次挂起的位置。

Kotlin 的基类 BaseContinuationImpl 就实现了这种恢复逻辑:

internal abstract class BaseContinuationImpl(
    public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
    public final override fun resumeWith(result: Result<Any?>) {
        var current = this
        var param = result
        while (true) {
            probeCoroutineResumed(current)
            with(current) {
                val completion = completion!!
                val outcome: Result<Any?> =
                    try {
                        val outcome = invokeSuspend(param) // 继续执行状态机
                        if (outcome === COROUTINE_SUSPENDED) return
                        Result.success(outcome)
                    } catch (exception: Throwable) {
                        Result.failure(exception)
                    }
                releaseIntercepted()
                if (completion is BaseContinuationImpl) {
                    // 尾递归优化,继续向上层恢复
                    current = completion
                    param = outcome

启动协程

回到最开始的 createCoroutine。它返回的其实是一个 SafeContinuation,这是对 suspend { ... } 编译后实现类的装饰。SafeContinuation 内部保存了一个 delegate,就是前面提到的 SuspendLambda 实现类。

当我们调用 resume(Unit) 时,会进入 SafeContinuation.resumeWith()

public actual override fun resumeWith(result: Result<T>) {
    while (true) { // lock-free loop
        val cur = this.result // atomic read
        when {
            cur === UNDECIDED -> if (RESULT.compareAndSet(this, UNDECIDED, result.value)) return
            cur === COROUTINE_SUSPENDED -> if (RESULT.compareAndSet(this, COROUTINE_SUSPENDED, RESUMED)) {
                delegate.resumeWith(result) // 启动协程体
                return
            }
            else -> throw IllegalStateException("Already resumed")
        }
    }
}

也就是说:

  • 首次调用时,SafeContinuation 会把调用转发给 delegate,也就是 SuspendLambdaresumeWith

  • 后者再调用 invokeSuspend(),从状态机的 case 0 开始执行协程体。

  • 如果中途遇到挂起点,就返回 COROUTINE_SUSPENDED 并保存状态;恢复时再继续执行下一个 case。

到这里,Kotlin 协程的 “创建 → 挂起 → 恢复 → 启动” 的完整闭环就串起来了。

Kotlin 协程的原理是什么?

Kotlin 协程的本质其实就是编译器自动生成的状态机 + Continuation 回调机制。我们平时写的 suspend 关键字和协程体代码,在编译后会被转换成一个实现类(例如 SuspendLambda),它继承自 ContinuationImpl,并实现了核心的 invokeSuspend() 方法。这个方法内部通过 label 字段来标记协程的执行位置,从而实现 挂起 → 恢复 的过程。


九、官方协程框架

协程作用域

launch 和 async

调度器

Channel

什么是 Channel?

在协程世界中,Channel 是一种非阻塞的通信机制,用来在多个协程之间传递消息。它可以理解为 BlockingQueue + 挂起函数 的结合体:

  • 发送端 (send()) 在必要时会挂起,直到消息被接收。

  • 接收端 (receive()) 在没有消息可取时也会挂起,直到有新的数据到达。

这样,Channel 就成了协程之间的“管道”,帮助我们实现生产者-消费者模式或协程之间的数据流转。

Channel 的分类

Kotlin 官方提供了多种不同策略的 Channel,适用于不同的场景:

  • Channel.RENDEZVOUS:不见不散,send 调用会挂起,直到有 receive 对应消费;

  • Channel.UNLIMITED:无限容量,消息直接放入队列,send 永远不会挂起(可能导致内存占用过大);

  • Channel.CONFLATED:保留最新,如果消费者处理不过来,那么旧值会被丢弃,消费者始终拿到最新的那条消息;

  • Channel.BUFFERED:带缓冲区,默认容量为 64,可以通过参数修改大小。超过容量时 send 会挂起。

val channel1 = Channel<Int>(Channel.RENDEZVOUS)    // 不见不散
val channel2 = Channel<Int>(Channel.UNLIMITED)    // 无限容量
val channel3 = Channel<Int>(Channel.CONFLATED)    // 保留最新
val channel4 = Channel<Int>(Channel.BUFFERED)    // 默认容量

Channel 的使用

执行流程:

  1. 生产者依次 send 数据;

  2. 消费者通过 receiveCatching 持续读取,直到 Channel 关闭并数据消费完毕;

  3. close() 后,发送端再调用 send 会抛异常,接收端则逐步取完缓冲区后结束。

import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

val logger = KotlinLogging.logger { }

suspend fun main() {
    val channel = Channel<Int>(Channel.RENDEZVOUS)
    
    // 生产者
    val producer = GlobalScope.launch {
        for (i in 0..3) {
            logger.debug { "sending $i" }
            channel.send(i)
            logger.debug { "sent $i" }
        }
        channel.close() // 关闭 Channel
    }
    
    // 消费者
    val consumer = GlobalScope.launch {
        while (isActive) {
            logger.debug { "receiving" }
            val value = channel.receiveCatching().getOrNull() ?: break
            logger.debug { "received $value" }
        }
    }

    producer.join()
    consumer.join()
}

Channel 的关闭

调用 close() 方法会关闭 Channel。此时有几点需要注意:

  • 关闭后还能接收未消费的数据:如果缓冲区里有剩余消息,可以继续 receive;

  • 关闭后无法再发送数据:再次调用 send 会抛出异常。

推荐使用下面的方式接收数据,当 Channel 关闭且数据取完时会返回 null,避免异常中断。

val value = channel.receiveCatching().getOrNull()

channel 的迭代

Channel 还支持使用 for 循环迭代,非常优雅:

suspend fun consume(channel: Channel<Int>) {
    for (value in channel) {
        println("received $value")
    }
    println("channel closed, iteration ended")
}

Select

Flow