# 深入 Kotlin 预计时间 3d 确保你已经了解所有的基础语法,这很重要。 ## 更全面的 class 用法 ### 更加详细的 class 构造函数与对象初始化 #### 类初始化 在 Kotlin 的类定义中可以写 `init {}` 代码块,它发生在构造函数之后,作用是使用构造函数定义的成员变量值再做一些构造函数没有的操作,比如利用构造函数的参数再做一些细致的变量处理。 #### 构造函数 构造函数有多种写法: 1. 直接写在类名之后 ```kotlin class MyClass( val a: Int, private var b: String, c: Int // 这里的变量 c 仅用于成员初始化,随后被抛弃,不作为成员变量 ) { val d = c * a // something else } ``` ```kotlin class MyClass private constructor( // 非私有时,constructor 可省 // ... ) { // something else } ``` 这是最方便的方法,我如果不用私有构造函数基本就是这样写,再使用默认值配合。 2. 写在类定义之内 ```kotlin class MyClass { val d: Int constructor( val a: Int, private var b: String, c: Int ) { d = c * a } // something else } ``` 3. 构造函数调用其他构造函数 ```kotlin class MyClass( var name: String?, val amount: Int ) { constructor( val amount: Int ) : this("Unknown", amount) // something else } ``` ### 伴生对象 定义方法: ```kotlin class MyClass() { companion object {} } ``` 它的使用和 object 完全一致,用于替代 Java 中的 static 修饰符,是一个非常方便的东西。可以省去工厂类,直接在伴生对象里定义工厂函数。 ### 类中类 #### 嵌套类 你可以在类中定义任何类,它们的调用需要带上外层类,或者在 import 中多写一层外层类名: ```kotlin class OuterClass() { class InnerClass() {} } // 如果只 import 外层类 import pack.age.OuterClass val a = OuterClass.InnerClass() // 如果想 import 内层类 import pack.age.OuterClass.InnerClass val a = InnerClass() ``` #### 内部类 内部类的定义需要在 class 前添加一个 `inner` 关键字。 内部类的特点是:它可以使用外部类的任意属性,包括私有的。有时会很方便。 如果出现同名属性可以使用标签来指明是哪一个类的属性: ```kotlin class MainActivity { fun showToast() { println("display") } inner class MyButton { fun showToast() {} fun setOnClickListener() { this@MainActivity.showToast() } // @ 后面写类名,用法与 this 一致 } } ``` ### 类继承细节 在 Kotlin 中,一个类最多只能直接继承一个类,但可以继承任意多个接口 #### 子类 如果类被继承,其中的类中类不会有任何影响,子类不含有类中类的任何信息。 Kotlin 中所有的类,成员变量,函数都是不可被继承,不可被重写的。如果需要继承 / 重写,只要在定义的开头加上 `open`。而接口 interface 的任意属性包括它自己默认就是 open 的。 在一个类的子类中可以使用它的父类(超类)的任意属性,用 `super` 代指,用法与 this 一致。 ```kotlin class FilledRectangle : Rectangle() { override fun draw() { super.draw() println("Filling the rectangle") } val fillColor: String get() = super.borderColor } ``` 这里,父类名后括号的含义是指,子类使用与父类一致的构造函数,且没有参数。 如果一个类继承的类 / 接口中含有重名的属性,那么子类必须重写。你可以在子类中以这种方式使用来自不同定义的属性: ```kotlin open class Rectangle { open fun draw() {} } interface Polygon { // interface 默认 open fun draw() {} } class Square() : Rectangle(), Polygon { override fun draw() { super.draw() // 调用 Rectangle.draw() super.draw() // 调用 Polygon.draw() } } ``` #### 接口 接口里面可以有: - 抽象函数(没有函数体) - 带默认实现的函数 - 抽象属性 - 一个写明类型的变量,但不能有默认值 ```kotlin interface Clickable { fun click() fun showOff() = println("I'm clickable") val name: String } ``` #### 抽象类 抽象类介于接口和普通类之间: - 可以有构造参数、状态、成员变量 - 可以有抽象成员,也可以有具体实现,如果成员变量有默认值,那它就不是`抽象的` - 抽象成员默认是 `open` 的,子类必须 `override` - 一个类只能继承一个抽象类,但可以实现多个接口 ```kotlin abstract class Animal { abstract val name: String abstract fun sound() open fun info() = "$name says ${sound()}" } class Dog(override val name: String) : Animal() { override fun sound() = "Woof" } ``` #### 密封 密封类 / 接口是指在定义的时候前面加上了 `sealed` 关键字。 - 受限继承:密封类的子类是有限的。所有的直接子类都必须在与密封类相同的文件中声明。 - 自身是抽象的:sealed 类本身是抽象的,不能直接被实例化。其构造器默认是 protected 级别。 - **配合 when 表达式绝佳**:在 when 语句中使用密封类时,如果处理了所有可能的子类情况,就不需要再写 else 分支。如果以后添加了新子类,编译器会给出提示。 例如: ```kotlin sealed class Result { data class Success(val data: T) : Result() data class Error(val exception: Throwable) : Result() object Loading : Result() } // 使用 when 进行处理(无需 else 分支) fun handleResult(result: Result) { when (result) { is Result.Success -> println("成功: ${result.data}") is Result.Error -> println("失败: ${result.message}") Result.Loading -> println("加载中...") } // 单例判断不加 is } ``` ### 泛型类 泛型类把类型作为参数: ```kotlin class Box(var value: T) val stringBox = Box("hello") val intBox = Box(123) ``` #### 类型约束 ```kotlin class NumberBox(val value: T) class NumberBox(val value: T) where T : Serializable, T : Number ``` 使用 `where` 给 `T` 更多限定,这里,`T` 既是 `Serializable` 的子类,也是 `Number` 的子类。 #### 型变 ```kotlin // out:协变,只能返回 T 类型变量 class Producer(private val value: T) { fun get(): T = value } // in:逆变,只能接收 T 类型变量 class Consumer { fun consume(value: T) {} } ``` #### 星投影 具体的泛型类型,依据传进来的 list 参数,它允许任意的唯一类型。与 Any 不同的是,使用 Any 指 list 本身的泛型就是 Any。 ```kotlin fun printAll(list: List<*>) { list.forEach { println(it) } } ``` ### 特殊类 #### object 它常用于单例、匿名对象、伴生对象。 ##### 单例 单例可以直接用类名当成对象变量名使用,而且每次使用时调用的对象都是同一个。 ```kotlin object Config { const val STR = "const" // object 里面可以定义 const 变量 var debug = false fun log(msg: String) { if (debug) println(msg) } } ``` ##### 匿名对象 它用于实现一个接口,或是别的什么东西。 ```kotlin val runnable = object : Runnable { override fun run() { println("running") } } ``` ##### 伴生对象 ```kotlin class User private constructor(val name: String) { companion object { const val TABLE = "users" fun create(name: String) = User(name) } // 用一个工厂方法代替构造函数,这很常用,它可以做一些更复杂的事。 } val u = User.create("Tom") println(User.TABLE) ``` #### data class 数据类自动生成: - `equals()` - `hashCode()` - `toString()` - `copy()` - `componentN()` ```kotlin data class User(val name: String, var age: Int) val u = User("Tom", 18) val u2 = u.copy(age = 19) val (name, age) = u ``` - 主构造至少有一个参数 - 主构造参数必须定义一个成员变量 - 不能是 `abstract`、`open`、`sealed`、`inner` #### enum class 枚举类表示一组有限常量。 ```kotlin enum class Direction( val degree: Int // 它指的是枚举变量的属性 ) { NORTH(0), EAST(90), SOUTH(180), WEST(270); // entries 是 Direction 的成员变量,是所有枚举变量组成的列表 fun turnRight(): Direction = entries[(ordinal + 1) % entries.size] } val d = Direction.NORTH println(d.degree) ``` 枚举可以实现接口,但不能继承普通类。每个枚举常量本身是对象。 ### 特殊类函数 #### 重写 toString ```kotlin class Point(val x: Int, val y: Int) { override fun toString(): String = "Point($x, $y)" } println(Point(1, 2)) // Point(1, 2) ``` `data class` 会自动生成 `toString()`。 #### 重载运算符 用 `operator` 修饰函数,让对象支持运算符。常见映射: | 运算符 | 函数 | | :-------: | :-----------: | | + | plus | | - | minus | | * | times | | / | div | | % | rem | | += 等 | plusAssign 等 | | [] | get / set | | () | invoke | | in | contains | | < > <= >= | compareTo | | .. | rangeTo | | == | equals | `compareTo` 函数较为特殊,它返回 `Int`,意思是这样的:a < b 等价于 a.compareTo(b) < 0。这与 CPP / Java 是一样的。 更多函数名请查询 [运算符重载](https://book.kotlincn.net/text/operator-overloading.html) ```kotlin data class Vec(val x: Int, val y: Int) { operator fun plus(other: Vec) = Vec(x + other.x, y + other.y) operator fun get(index: Int) = if (index == 0) x else y } val v = Vec(1, 2) + Vec(3, 4) println(v[0]) ``` #### component 函数 `componentN()` 用于解构声明。 ```kotlin class User(val name: String, val age: Int) { operator fun component1() = name operator fun component2() = age } val (name, age) = User("Tom", 18) ``` `data class` 会自动生成 `component1()`、`component2()` 等。 #### invoke 函数 `invoke` 让实例可以像函数一样调用,类似 Python 的 `__call__()`。 ```kotlin class Adder(val base: Int) { operator fun invoke(x: Int): Int = base + x } val add10 = Adder(10) println(add10(5)) // 15 ``` ## 特殊函数 ### 嵌套 lambda 与标签 标签语法:`label@`。 ```kotlin outer@ for (i in 1..3) { for (j in 1..3) { if (j == 2) continue@outer if (i == 3) break@outer } } ``` lambda 中常用 `return@label`: ```kotlin listOf(1, 2, 3).forEach lit@{ if (it == 2) return@lit println(it) } ``` 如果 lambda 是 `inline` 的,可以直接非局部返回: ```kotlin fun foo() { listOf(1, 2, 3).forEach { if (it == 2) return // 从 foo 返回 } } ``` 嵌套 lambda 中同名隐式标签可能冲突,建议显式命名标签。 ### 泛型函数 ```kotlin fun singletonList(item: T): List = listOf(item) fun > max(a: T, b: T): T = if (a > b) a else b ``` 多个约束用 `where`: ```kotlin fun ensureTrailingPeriod(seq: T) where T : CharSequence, T : Appendable { // 在这个函数内,T 需要同时为 CharSequence 和 Appendable 的子类。 // 这样,seq 就能调用两个父类的函数。 if (!seq.endsWith('.')) seq.append('.') } ``` ### infix 函数 `infix` 函数必须是成员函数或扩展函数,并且: - 只有一个参数 - 参数不能是可变参数 - 参数不能有默认值 ```kotlin infix fun String.hasPrefix(prefix: String): Boolean = startsWith(prefix) val ok = "Kotlin" hasPrefix "Kot" // infix 函数有独特写法 ``` 标准库中的 `to` 也是 infix: ```kotlin val pair = "name" to "Tom" ``` ### inline 函数 `inline` 会把函数体和 lambda 参数内联到调用处,减少 lambda 对象开销,并允许非局部返回。 ```kotlin inline fun withLock(lock: Lock, block: () -> T): T { lock.lock() try { return block() } finally { lock.unlock() } } ``` `noinline`:不内联某个 lambda 参数,可把它当对象传递。 ```kotlin inline fun foo(noinline block: () -> Unit) { val f = block f() } ``` `crossinline`:内联 lambda 中禁止非局部返回。 ```kotlin inline fun foo(crossinline block: () -> Unit) { Runnable { block() }.run() } ``` ### 泛型 reified 普通泛型有类型擦除,不能直接 `is T` 或 `T::class`。 配合 `inline` 和 `reified` 可以保留类型信息。 ```kotlin inline fun Any.isInstanceOf(): Boolean = this is T inline fun List<*>.filterIsInstance(): List { val result = mutableListOf() for (item in this) { if (item is T) result.add(item) } return result } ``` `reified` 只能用于 `inline` 函数。 ### 扩展函数 扩展函数可以在不修改原类的情况下,为类增加函数。 ```kotlin fun String.lastChar(): Char = this[length - 1] val String.lastChar: Char get() = this[length - 1] ``` 扩展函数是独立于类定义的,使用时也需要额外的 `import`。 ```kotlin open class A class B : A() fun A.foo() = "A" fun B.foo() = "B" val a: A = B() println(a.foo()) // A ``` 成员函数优先于扩展函数: ```kotlin class C { fun foo() = "member" } fun C.foo() = "extension" println(C().foo()) // member ``` #### 概念与 lambda 变量类型 普通函数类型: ```kotlin val f1: (String) -> Int = { it.length } ``` 带接收者的函数类型: ```kotlin val f2: String.() -> Int = { length } println("abc".f2()) // 3 println(f2("abc")) // 3 ``` 区别: - `(String) -> Int`:参数在 lambda 中用 `it` 或命名参数访问 - `String.() -> Int`:接收者在 lambda 中用 `this` 访问,而且,相当与直接在类 `String` 中定义函数 #### 常用的标准库扩展函数 ##### apply 它的返回值是对象本身,lambda 相当于类函数。 ```kotlin val user = User().apply { name = "Alice" age = 18 } // 修改对象属性后返回 user 对象本身 ``` ##### also 它的返回值是对象本身,lambda 相当于参数是同类的函数。 ```kotlin val numbers = mutableListOf(1, 2, 3).also { println("The list elements were: $it") } // numbers 仍是是可变 List ``` ##### let 可以利用 `let` 让一段代码只在对象非空时执行: ```kotlin val name: String? = "Kotlin" name?.let { println("长度是 ${it.length}") } ``` 如果 `name` 是 `null`,`let` 里面的代码不会执行。 ##### run 它的返回值是最后的任意结果,lambda 相当于类函数。 ```kotlin val result = StringBuilder().run { append("Hello, ") append("World!") toString() // 返回结果 } ``` ##### use 它的返回值是对象本身,lambda 相当于参数是同类的函数。 但是仅用于继承了 AutoCloseable 的类,相当于 Java 的 try-with-resources。 ```kotlin val fileContent = File(filePath) .bufferedReader() .use { reader -> reader.readText() } // use 会自动调用 BufferedReader 的 close() 方法 ``` ### suspend 函数 如果你的函数内部需要做一些不得不等待的事,或者需要很久的事的话,那么你可以选择给它添加 `suspend` 关键字。这样它可以在任意时间暂停。 如果你想要调用一个 suspend 函数,那么你需要注意,它不能直接写在任何非 suspend 函数中。 如果你非要在一个普通函数中使用 suspend 函数,那么你可以使用 `runBlocking`,它只有一个参数,一个 lambda,可以运行 suspend 函数并阻塞线程直到 lambda 运行完毕。 更细致的讲解在 [成为 Kotlin 超人](./成为Kotlin超人.md) 的**协程**中。 ## 文件 IO Kotlin 的文件 IO 在 java.io.File 的基础上提供了许多充满 Kotlin 风味的方法。 ```kotlin val file = File("example.txt") // 拿到文件(来自 Java) val content: String = file.readText() // 默认 UTF-8 val lines: List = file.readLines() file.forEachLine { line -> println(line) } file.writeText("Hello, Kotlin!") // 完全覆盖 file.appendText("\nNew line data.") // 末尾添加 File("my_folder").walk().forEach { file -> println(file.absolutePath) // 遍历文件夹 } val source = File("source.txt") val dest = File("dest.txt") source.copyTo(dest, overwrite = true) // 复制文件 ``` ## 用 Gradle 加载依赖(外部库) ### build.gradle.kts 这是最常被修改的 gradle 配置文件,里面不只有依赖的设置,还有一些 JVM / Kotlin 的设置。这里不做详细介绍。对于一个普通的 Kotlin 项目,控制依赖的部分写在这里: ```kotlin dependencies { implementation("org.jetbrains.kotlin:kotlin-reflect") // 加载最新版的 Kotlin 反射,但不是所有的库都能自动添加版本号 implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") // 加载0.6.0版本的 Kotlin 时间 } ``` 依赖的名称一般从 Maven 仓库中查找,不少在 GitHub 有仓库的 Kotlin 库会写明自己在 Maven 上的名称。 有时会用到 `testImplementation`,它只在测试源文件中起作用。 ### settings.gradle.kts 这个文件用的很少,我的建议是,一般别动。 ## 时间 在 Kotlin 中可以直接使用 Java 的时间处理方法,如果需要实现复杂一点的逻辑或是需要用到 Kotlin 序列化(在[成为 Kotlin 超人](./成为Kotlin超人.md)中有讲解),更推荐使用 `kotlinx-datetime`。 官方文档:[kotlinx-datetime](https://kotlinlang.org/api/kotlinx-datetime/kotlinx-datetime/) ### 加载依赖 可以在官方文档中找到目前的版本,随后在 build.gradle.kts 中写: ```kotlin dependencies { implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") } ``` ### 拿到当前时间或任意时间 ```kotlin val nowInstant = Clock.System.now() // 获取当前时刻 (UTC) val customDate = LocalDate(2026, 10, 1) // 也可以写成 LocalDate(2026, Month.OCTOBER, 1) val customTime = LocalTime(14, 30, 0) // 时, 分, 秒, 纳秒 val customDateTime = LocalDateTime(2026, 10, 1, 14, 30, 0) println(customDate) // 2026-10-01 println(customDateTime) // 2026-10-01T14:30:00 println(nowInstant) // 2026-09-19T08:39:08.123Z println(nowInstant.toEpochMilliseconds()) // 毫秒时间戳 ``` ### 分析日期 ```kotlin val localDateTime = currentMoment.toLocalDateTime(TimeZone.currentSystemDefault()) println(localDateTime) // 2026-09-19T17:39:08.123 println(localDateTime.year) // 年: 2026 println(localDateTime.monthNumber) // 月: 9 println(localDateTime.dayOfMonth) // 日: 19 println(localDateTime.hour) // 时: 17 println(localDateTime.minute) // 分: 39 println(localDateTime.today) // 2026-09-19 println(localDateTime.dayOfWeek) // SATURDAY 这是一个枚举类 DayOfWeek ``` ## Kotlin 反射 ### 拿到类变量 Kotlin 类变量的类型是 `KClass`,可以通过一个类的类名直接得到,例如 ```kotlin val c = MyClass::class val javaClass = MyClass::class.java ``` 也可以通过实际变量拿到精确类名: ```kotlin val a: Number = 1 // Int 是 Number 子类 println(a::class.qualifiedName) // kotlin.Int ``` ### 利用类变量获得 / 修改对象 ```kotlin var x = 1 fun main() { println(::x.get()) // 1 println(::x.name) // x ::x.set(2) // 将它设为 2 } ``` 这里 `::x` 引用到的是本类中的 `x` 变量,也就是说如果 `x` 定义在函数 `main` 中,就不能这么引用了。注意,只有可变变量才能用 `set()`,除非使用 `Java 反射`强行更改。Kotlin 反射不推荐如此操作。 ### 复杂类反射 ```kotlin class A(val p: Int) val a = A(1) val prop = A::p println(prop.get(a)) // 1 // 也可以拿到它的 Java 定义。 println(A::p.javaGetter) // public final int A.getP() println(A::p.javaField) // private final int A.p ``` 通过这种方法,可以拿到一个类对象的一个变量值。 ```kotlin val lambda = ::A // 拿到类 A 的无参构造函数 ``` 这个 lambda 相当于 A(),如果 A 的构造函数必须有参数(有默认值的话算作可以没有参数)。那么 lambda 是不能调用的。 ```kotlin class Person(val name: String, age: Int) val kClass = Person::class val constructor = kClass.primaryConstructor // 这是主构造函数,是第一个被定义的,而且可以填入参数 // 这样,你可以拿到它的全部构造函数 for (constructor in kClass.constructors) { val parameters = constructor.parameters // 获取构造函数的参数列表 if (parameters.size == 2) { val instance = constructor.call("李四", 20) break } } ``` 这里的 parameters 是一个列表,它里面的元素是用来描述构造函数的每一个参数的: ```kotlin println("参数位置: ${param.index}") println("参数名称: ${param.name}") println("参数类型: ${param.type}") println("是否为可选参数(有默认值): ${param.isOptional}") println("是否为可变长参数: ${param.isVararg}") ``` ## 定义一个特殊的变量/函数名 只需要使用反引号 ``` ` ``` 将一个任意字符串包裹起来,它几乎可以被定义为任何东西的名称。 ```kotlin fun `fun`() = "fun" // 例如定义一个函数 val `this` = 1 // 或是一个变量 val `or write a sentence` = true ``` ## 类型别名 typealias 当某些类型名字特别长,或者某些 lambda 函数类型签名较长时,写起来会很啰嗦。使用 `typealias` 可以给现有类型起一个简单易读的别名。 ```kotlin // 给复杂的函数类型起别名 typealias ClickListener = (viewId: Int, message: String) -> Unit fun setOnClick(listener: ClickListener) { listener(1, "clicked") } // 给长泛型起别名 typealias UserMap = Map>> // 在 KMP 中非常关键:直接将平台已有类型映射给 commonMain 的公共声明 expect class AtomicInt actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger ``` ## Nothing 类型与 TODO() 在 Kotlin 中,`Nothing` 是一个非常特殊的底层类型:它是**所有其他类型的子类型(Bottom type)**。 它表示“这个函数永远不会正常返回”,比如抛出异常或者死循环的函数: ```kotlin fun fail(message: String): Nothing { throw IllegalArgumentException(message) } ``` 因为 `Nothing` 是所有类型的子类,所以在类型推断和空安全配合时非常方便: ```kotlin // 1. Elvis 操作符配合抛异常,编译器会自动把 Noting 识别为非空 String val name: String = inputName ?: fail("Name is required") // 2. TODO() 占位 fun calculateComplexLogic(): Int { TODO("还没写完,占个位编译不报错") // TODO() 的返回值就是 Nothing } ``` ## IDEA 的常用快捷键 `ctrl + alt + L`:格式化代码 `ctrl + alt + O`:整理文件中的 import `alt + enter`:光标在警告 / 报错上时显示快速处理方案或光标处的格式转换 `shift + F6`:重命名文件名 / 属性名 ## 接下来可以看 [目录](../README.md) [下一篇:Kotlin Springboot](./Kotlin-Springboot.md) [下一篇:Kotlin Ktor](./Kotlin-Ktor.md) [更进一步:成为 Kotlin 超人](./成为Kotlin超人.md) [进入 KMP](./进入KMP.md) [KMP:复杂逻辑与多平台不同逻辑](./复杂逻辑与多平台不同逻辑.md)