23 KiB
深入 Kotlin
预计时间 3d
确保你已经了解所有的基础语法,这很重要。
更全面的 class 用法
更加详细的 class 构造函数与对象初始化
类初始化
在 Kotlin 的类定义中可以写 init {} 代码块,它发生在构造函数之后,作用是使用构造函数定义的成员变量值再做一些构造函数没有的操作,比如利用构造函数的参数再做一些细致的变量处理。
构造函数
构造函数有多种写法:
- 直接写在类名之后
class MyClass(
val a: Int,
private var b: String,
c: Int // 这里的变量 c 仅用于成员初始化,随后被抛弃,不作为成员变量
) {
val d = c * a
// something else
}
class MyClass private constructor( // 非私有时,constructor 可省
// ...
) {
// something else
}
这是最方便的方法,我如果不用私有构造函数基本就是这样写,再使用默认值配合。
- 写在类定义之内
class MyClass {
val d: Int
constructor(
val a: Int,
private var b: String,
c: Int
) {
d = c * a
}
// something else
}
- 构造函数调用其他构造函数
class MyClass(
var name: String?,
val amount: Int
) {
constructor(
val amount: Int
) : this("Unknown", amount)
// something else
}
伴生对象
定义方法:
class MyClass() {
companion object {}
}
它的使用和 object 完全一致,用于替代 Java 中的 static 修饰符,是一个非常方便的东西。可以省去工厂类,直接在伴生对象里定义工厂函数。
类中类
嵌套类
你可以在类中定义任何类,它们的调用需要带上外层类,或者在 import 中多写一层外层类名:
class OuterClass() {
class InnerClass() {}
}
// 如果只 import 外层类
import pack.age.OuterClass
val a = OuterClass.InnerClass()
// 如果想 import 内层类
import pack.age.OuterClass.InnerClass
val a = InnerClass()
内部类
内部类的定义需要在 class 前添加一个 inner 关键字。
内部类的特点是:它可以使用外部类的任意属性,包括私有的。有时会很方便。
如果出现同名属性可以使用标签来指明是哪一个类的属性:
class MainActivity {
fun showToast() {
println("display")
}
inner class MyButton {
fun showToast() {}
fun setOnClickListener() {
this@MainActivity.showToast()
} // @ 后面写类名,用法与 this 一致
}
}
类继承细节
在 Kotlin 中,一个类最多只能直接继承一个类,但可以继承任意多个接口
子类
如果类被继承,其中的类中类不会有任何影响,子类不含有类中类的任何信息。
Kotlin 中所有的类,成员变量,函数都是不可被继承,不可被重写的。如果需要继承 / 重写,只要在定义的开头加上 open。而接口 interface 的任意属性包括它自己默认就是 open 的。
在一个类的子类中可以使用它的父类(超类)的任意属性,用 super 代指,用法与 this 一致。
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
这里,父类名后括号的含义是指,子类使用与父类一致的构造函数,且没有参数。
如果一个类继承的类 / 接口中含有重名的属性,那么子类必须重写。你可以在子类中以这种方式使用来自不同定义的属性:
open class Rectangle {
open fun draw() {}
}
interface Polygon { // interface 默认 open
fun draw() {}
}
class Square() : Rectangle(), Polygon {
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}
接口
接口里面可以有:
- 抽象函数(没有函数体)
- 带默认实现的函数
- 抽象属性
- 一个写明类型的变量,但不能有默认值
interface Clickable {
fun click()
fun showOff() = println("I'm clickable")
val name: String
}
抽象类
抽象类介于接口和普通类之间:
- 可以有构造参数、状态、成员变量
- 可以有抽象成员,也可以有具体实现,如果成员变量有默认值,那它就不是
抽象的 - 抽象成员默认是
open的,子类必须override - 一个类只能继承一个抽象类,但可以实现多个接口
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 分支。如果以后添加了新子类,编译器会给出提示。
例如:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Throwable) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// 使用 when 进行处理(无需 else 分支)
fun handleResult(result: Result<String>) {
when (result) {
is Result.Success -> println("成功: ${result.data}")
is Result.Error -> println("失败: ${result.message}")
Result.Loading -> println("加载中...")
} // 单例判断不加 is
}
泛型类
泛型类把类型作为参数:
class Box<T>(var value: T)
val stringBox = Box("hello")
val intBox = Box(123)
类型约束
class NumberBox<T : Number>(val value: T)
class NumberBox<T>(val value: T) where T : Serializable, T : Number
使用 where 给 T 更多限定,这里,T 既是 Serializable 的子类,也是 Number 的子类。
型变
// out:协变,只能返回 T 类型变量
class Producer<out T>(private val value: T) {
fun get(): T = value
}
// in:逆变,只能接收 T 类型变量
class Consumer<in T> {
fun consume(value: T) {}
}
星投影
具体的泛型类型,依据传进来的 list 参数,它允许任意的唯一类型。与 Any 不同的是,使用 Any 指 list 本身的泛型就是 Any。
fun printAll(list: List<*>) {
list.forEach { println(it) }
}
特殊类
object
它常用于单例、匿名对象、伴生对象。
单例
单例可以直接用类名当成对象变量名使用,而且每次使用时调用的对象都是同一个。
object Config {
const val STR = "const" // object 里面可以定义 const 变量
var debug = false
fun log(msg: String) {
if (debug) println(msg)
}
}
匿名对象
它用于实现一个接口,或是别的什么东西。
val runnable = object : Runnable {
override fun run() {
println("running")
}
}
伴生对象
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()
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
枚举类表示一组有限常量。
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
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 是一样的。
更多函数名请查询 运算符重载
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() 用于解构声明。
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__()。
class Adder(val base: Int) {
operator fun invoke(x: Int): Int = base + x
}
val add10 = Adder(10)
println(add10(5)) // 15
特殊函数
嵌套 lambda 与标签
标签语法:label@。
outer@ for (i in 1..3) {
for (j in 1..3) {
if (j == 2) continue@outer
if (i == 3) break@outer
}
}
lambda 中常用 return@label:
listOf(1, 2, 3).forEach lit@{
if (it == 2) return@lit
println(it)
}
如果 lambda 是 inline 的,可以直接非局部返回:
fun foo() {
listOf(1, 2, 3).forEach {
if (it == 2) return // 从 foo 返回
}
}
嵌套 lambda 中同名隐式标签可能冲突,建议显式命名标签。
泛型函数
fun <T> singletonList(item: T): List<T> = listOf(item)
fun <T : Comparable<T>> max(a: T, b: T): T = if (a > b) a else b
多个约束用 where:
fun <T> ensureTrailingPeriod(seq: T)
where T : CharSequence, T : Appendable {
// 在这个函数内,T 需要同时为 CharSequence 和 Appendable 的子类。
// 这样,seq 就能调用两个父类的函数。
if (!seq.endsWith('.')) seq.append('.')
}
infix 函数
infix 函数必须是成员函数或扩展函数,并且:
- 只有一个参数
- 参数不能是可变参数
- 参数不能有默认值
infix fun String.hasPrefix(prefix: String): Boolean = startsWith(prefix)
val ok = "Kotlin" hasPrefix "Kot" // infix 函数有独特写法
标准库中的 to 也是 infix:
val pair = "name" to "Tom"
inline 函数
inline 会把函数体和 lambda 参数内联到调用处,减少 lambda 对象开销,并允许非局部返回。
inline fun <T> withLock(lock: Lock, block: () -> T): T {
lock.lock()
try {
return block()
} finally {
lock.unlock()
}
}
noinline:不内联某个 lambda 参数,可把它当对象传递。
inline fun foo(noinline block: () -> Unit) {
val f = block
f()
}
crossinline:内联 lambda 中禁止非局部返回。
inline fun foo(crossinline block: () -> Unit) {
Runnable { block() }.run()
}
泛型 reified
普通泛型有类型擦除,不能直接 is T 或 T::class。
配合 inline 和 reified 可以保留类型信息。
inline fun <reified T> Any.isInstanceOf(): Boolean = this is T
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (item is T) result.add(item)
}
return result
}
reified 只能用于 inline 函数。
扩展函数
扩展函数可以在不修改原类的情况下,为类增加函数。
fun String.lastChar(): Char = this[length - 1]
val String.lastChar: Char
get() = this[length - 1]
扩展函数是独立于类定义的,使用时也需要额外的 import。
open class A
class B : A()
fun A.foo() = "A"
fun B.foo() = "B"
val a: A = B()
println(a.foo()) // A
成员函数优先于扩展函数:
class C {
fun foo() = "member"
}
fun C.foo() = "extension"
println(C().foo()) // member
概念与 lambda 变量类型
普通函数类型:
val f1: (String) -> Int = { it.length }
带接收者的函数类型:
val f2: String.() -> Int = { length }
println("abc".f2()) // 3
println(f2("abc")) // 3
区别:
(String) -> Int:参数在 lambda 中用it或命名参数访问String.() -> Int:接收者在 lambda 中用this访问,而且,相当与直接在类String中定义函数
常用的标准库扩展函数
apply
它的返回值是对象本身,lambda 相当于类函数。
val user = User().apply {
name = "Alice"
age = 18
} // 修改对象属性后返回 user 对象本身
also
它的返回值是对象本身,lambda 相当于参数是同类的函数。
val numbers = mutableListOf(1, 2, 3).also {
println("The list elements were: $it")
} // numbers 仍是是可变 List
let
可以利用 let 让一段代码只在对象非空时执行:
val name: String? = "Kotlin"
name?.let {
println("长度是 ${it.length}")
}
如果 name 是 null,let 里面的代码不会执行。
run
它的返回值是最后的任意结果,lambda 相当于类函数。
val result = StringBuilder().run {
append("Hello, ")
append("World!")
toString() // 返回结果
}
use
它的返回值是对象本身,lambda 相当于参数是同类的函数。 但是仅用于继承了 AutoCloseable 的类,相当于 Java 的 try-with-resources。
val fileContent = File(filePath)
.bufferedReader()
.use { reader ->
reader.readText()
} // use 会自动调用 BufferedReader 的 close() 方法
suspend 函数
如果你的函数内部需要做一些不得不等待的事,或者需要很久的事的话,那么你可以选择给它添加 suspend 关键字。这样它可以在任意时间暂停。
如果你想要调用一个 suspend 函数,那么你需要注意,它不能直接写在任何非 suspend 函数中。
如果你非要在一个普通函数中使用 suspend 函数,那么你可以使用 runBlocking,它只有一个参数,一个 lambda,可以运行 suspend 函数并阻塞线程直到 lambda 运行完毕。
更细致的讲解在 成为 Kotlin 超人 的协程中。
文件 IO
Kotlin 的文件 IO 在 java.io.File 的基础上提供了许多充满 Kotlin 风味的方法。
val file = File("example.txt") // 拿到文件(来自 Java)
val content: String = file.readText() // 默认 UTF-8
val lines: List<String> = 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 项目,控制依赖的部分写在这里:
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 超人中有讲解),更推荐使用 kotlinx-datetime。
官方文档:kotlinx-datetime
加载依赖
可以在官方文档中找到目前的版本,随后在 build.gradle.kts 中写:
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
}
拿到当前时间或任意时间
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())
// 毫秒时间戳
分析日期
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<T>,可以通过一个类的类名直接得到,例如
val c = MyClass::class
val javaClass = MyClass::class.java
也可以通过实际变量拿到精确类名:
val a: Number = 1 // Int 是 Number 子类
println(a::class.qualifiedName) // kotlin.Int
利用类变量获得 / 修改对象
var x = 1
fun main() {
println(::x.get()) // 1
println(::x.name) // x
::x.set(2) // 将它设为 2
}
这里 ::x 引用到的是本类中的 x 变量,也就是说如果 x 定义在函数 main 中,就不能这么引用了。注意,只有可变变量才能用 set(),除非使用 Java 反射强行更改。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
通过这种方法,可以拿到一个类对象的一个变量值。
val lambda = ::A // 拿到类 A 的无参构造函数
这个 lambda 相当于 A(),如果 A 的构造函数必须有参数(有默认值的话算作可以没有参数)。那么 lambda 是不能调用的。
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 是一个列表,它里面的元素是用来描述构造函数的每一个参数的:
println("参数位置: ${param.index}")
println("参数名称: ${param.name}")
println("参数类型: ${param.type}")
println("是否为可选参数(有默认值): ${param.isOptional}")
println("是否为可变长参数: ${param.isVararg}")
定义一个特殊的变量/函数名
只需要使用反引号 ` 将一个任意字符串包裹起来,它几乎可以被定义为任何东西的名称。
fun `fun`() = "fun" // 例如定义一个函数
val `this` = 1 // 或是一个变量
val `or write a sentence` = true
类型别名 typealias
当某些类型名字特别长,或者某些 lambda 函数类型签名较长时,写起来会很啰嗦。使用 typealias 可以给现有类型起一个简单易读的别名。
// 给复杂的函数类型起别名
typealias ClickListener = (viewId: Int, message: String) -> Unit
fun setOnClick(listener: ClickListener) {
listener(1, "clicked")
}
// 给长泛型起别名
typealias UserMap = Map<String, List<Set<Int>>>
// 在 KMP 中非常关键:直接将平台已有类型映射给 commonMain 的公共声明
expect class AtomicInt
actual typealias AtomicInt = java.util.concurrent.atomic.AtomicInteger
Nothing 类型与 TODO()
在 Kotlin 中,Nothing 是一个非常特殊的底层类型:它是所有其他类型的子类型(Bottom type)。
它表示“这个函数永远不会正常返回”,比如抛出异常或者死循环的函数:
fun fail(message: String): Nothing {
throw IllegalArgumentException(message)
}
因为 Nothing 是所有类型的子类,所以在类型推断和空安全配合时非常方便:
// 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:重命名文件名 / 属性名