Loading... # Kotlin Kotlin 作为一个充满甜度拉满的语言,可以说是照顾到了开发者的方方面面。 这篇文章将带你盘点 Kotlin 的核心特性,并与 Java 展开对比。 备注:下文中所使用的“编译到 Java”或类似描述,实际上指的是 Kotlin 编译到 Java ByteCode 后为了演示方便而给出的伪代码。 # 特性一览 ## 属性 ### 属性的发展 属性 (Property),相对于字段 (Field) 来说,他是一个进步。 对于 Java 来说,使用 Getter/Setter 封装一个 Field 是已经形成行业认同的了 避免 Field 直接暴露,或是对读写进行业务附加。又或是制作只读/只写 Field。 从 Java 发展到 C#。 C# 发现了 Java 的 Getter/Setter 的问题。 于是引入了 Property。 但由于时代的局限,C# 不敢贸然直接移除 Field。 但从 C# 这么多年的发展来看。 很多通用生态已经只认 Property 不认 Field 了。 从 C# 到 Kotlin。 Kotlin 在 C# 的基础上更进一步。 既然 C# 的经验告诉了我们,Property 可以作为 Field 的完全上位替代。 那就直接砍了 Field。 ### 什么是属性 实际上的 Property,他与 Field 并没有直接关系。 Property 指的实际上是一组 Getter/Setter。 Property 可以有背后字段,也可以没有。 ``` Kotlin var hello: String = "123" ``` 这个 Property 就有一个实际上的 Field。 在 Java 中,这段代码应该是这样的。 ``` Java private String hello; public String getHello(){ return this.hello; } public void setHello(String value){ this.hello=value; } ``` 而,这种 Property 是没有背后字段的。 ``` Kotlin val hello: String get() = "123" ``` 这种属性因为不需要实际存储内容,是不会生成一个 Field 的。 在 Java 中,会只生成一个实际上的 `getHello` 函数。 ## 扩展函数/属性 ### 扩展函数 Kotlin 可以对某个类进行扩展函数。 ``` Kotlin fun String.hello(){ // 此时 this 会是调用扩展函数时的 String } // 可以将函数挂在到目标类下,实现快速操作。 "123".hello() ``` 实际上行为被编译到 Java 以后是。 ``` Java public static void hello(String self){ } hello("123") ``` 通过静态函数,和参数映射实现的。 ### 扩展属性 Kotlin 实际上不仅可以扩展类的函数,也可以扩展类的属性。 因为 属性 实际上是一组 Getter/Setter。 但因为 Kotlin 的扩展是通过静态函数和参数映射实现的。 所以扩展属性是没有背后字段的。 只能提供 Getter/Setter。 ``` Kotlin val String.charValue: CharArray get() = toCharArray() ``` 编译到 Java 也就是 ``` Java public static char[] getCharValue(String self){ return self.toCharArray() } ``` 扩展属性,和扩展函数其实一样。 都是通过静态函数和参数映射实现的。 ## 操作符&表达式 ### 操作符 在 Kotlin 中,被定义的操作符有: | 表达式 | 实际调用的函数 | |---|---| | `+a` | `a.unaryPlus()` | | `-a` | `a.unaryMinus()` | | `!a` | `a.not()` | | `a++` / `a--` | `a.inc()` / `a.dec()` | | `a + b` | `a.plus(b)` | | `a - b` | `a.minus(b)` | | `a * b` | `a.times(b)` | | `a / b` | `a.div(b)` | | `a % b` | `a.rem(b)` | | `a..b` | `a.rangeTo(b)` | | `a..<b` | `a.rangeUntil(b)` | | `a in b` | `b.contains(a)` | | `a[i]` | `a.get(i)` | | `a[i] = v` | `a.set(i, v)` | | `a()` | `a.invoke()` | | `a += b` | `a.plusAssign(b)` | | `a == b` | `a?.equals(b) ?: (b === null)` | | `a > b` | `a.compareTo(b) > 0` | | `val (x, y) = a` | `a.component1()`、`a.component2()` | 可以看出,操作符其实就是一组**约定好名字的函数**。 只要用 `operator` 修饰对应名字的函数,自己的类就能用上操作符。 ```Kotlin data class Vec(val x: Int, val y: Int) { operator fun plus(other: Vec) = Vec(x + other.x, y + other.y) } Vec(1, 2) + Vec(3, 4) // Vec(x=4, y=6) ``` 编译到 Java 以后就是一次普通的函数调用。 ```Java new Vec(1, 2).plus(new Vec(3, 4)); ``` 既然操作符就是函数,那它当然也能是**扩展函数**。 Kotlin 标准库就是这样给 `BigDecimal` 加上操作符的。 在 Java 中,计算 `(a + b) * c` 只能这样写: ```Java a.add(b).multiply(c); ``` 在 Kotlin 中: ```Kotlin (a + b) * c ``` 这里没有任何魔法。 只是标准库中存在这样一个扩展函数: ```Kotlin operator fun BigDecimal.plus(other: BigDecimal): BigDecimal = this.add(other) ``` #### == 与 === Java 里最经典的坑之一: ```Java String a = new String("123"); String b = new String("123"); a == b; // false,比较的是引用 a.equals(b); // true ``` Kotlin 把这两件事分开了。 `==` 调用的是 `equals`,并且已经帮你处理好了 null。 `===` 才是比较引用。 ```Kotlin a == b // true a === b // false ``` #### 中缀函数 除了这些预定义的操作符,Kotlin 还允许用 `infix` 定义“看起来像操作符”的函数。 ```Kotlin infix fun Int.add(other: Int) = this + other 1 add 2 // 3 ``` 我们常用的 `to` 就是一个中缀函数: ```Kotlin val map = mapOf(1 to "one", 2 to "two") ``` Kotlin 没有 `<<`、`>>`、`&`、`|` 这类位运算符。 取而代之的是 `shl`、`shr`、`and`、`or` 这些中缀函数。 ```Kotlin val flag = (1 shl 3) or 1 ``` ### 表达式 在 Java 中,`if`、`try` 都是**语句**,它们不产生值。 (`switch` 直到 Java 14 才有了表达式形式。) 所以在 Java 里,经常能看到这种写法: ```Java int num; try { num = Integer.parseInt(str); } catch (NumberFormatException e) { num = -1; } ``` 在 Kotlin 中,`if`、`when`、`try` 都是**表达式**,都可以产生值。 ```Kotlin val num = try { str.toInt() } catch (e: NumberFormatException) { -1 } val max = if (a > b) a else b val desc = when (score) { 100 -> "满分" in 90..99 -> "优秀" in 60..89 -> "及格" else -> "不及格" } ``` 也正因为 `if` 本身就是表达式,Kotlin 干脆就没有三元运算符 `? :`。 更进一步,`throw` 和 `return` 在 Kotlin 中也是表达式。 它们的类型是 `Nothing`,意思是“这里永远不会产生值”。 `Nothing` 是所有类型的子类型,所以它可以出现在任何需要值的地方: ```Kotlin val name = user.name ?: return val id = param ?: throw IllegalArgumentException("param 不能为空") ``` 函数体如果只有一个表达式,还可以直接用 `=` 连接: ```Kotlin fun sum(a: Int, b: Int) = a + b ``` ## 空安全 ### 十亿美元的错误 null 的发明者 Tony Hoare 曾称 null 引用是他“十亿美元的错误”。 写过 Java 的人,应该都被 `NullPointerException` 折磨过。 Kotlin 的做法是:**把“能否为 null”写进类型系统里。** ```Kotlin var a: String = "abc" a = null // 编译错误 var b: String? = "abc" b = null // OK b.length // 编译错误,b 可能为 null ``` `String` 和 `String?` 是两种不同的类型。 空指针的问题从运行期被提前到了编译期。 ### 安全调用与 Elvis 想获取一个用户所在的城市,在 Java 中: ```Java String city = null; if (user != null && user.getAddress() != null) { city = user.getAddress().getCity(); } ``` 在 Kotlin 中: ```Kotlin val city = user?.address?.city ``` `?.` 的意思是:不为 null 就调用,为 null 就直接返回 null。 再配合 `?:` (Elvis 运算符) 给出默认值: ```Kotlin val city = user?.address?.city ?: "未知" ``` 如果你非常确定它不为 null,也可以用 `!!` 强行断言。 不过一旦它真的是 null,你会喜提一个 NPE。 ### 编译后是什么样的? Kotlin 的可空信息会以 `@Nullable` / `@NotNull` 注解的形式保留在字节码中。 同时,对于非空参数的公开函数,Kotlin 会在函数开头插入检查: ```Kotlin fun hello(name: String) { } ``` ```Java public static final void hello(@NotNull String name) { Intrinsics.checkNotNullParameter(name, "name"); } ``` 这是为了防止 Java 代码调用时,往里塞一个 null。 ## 智能转换 在 Java 中(Java 16 的模式匹配之前),判断完类型后还需要手动强转: ```Java if (obj instanceof String) { int len = ((String) obj).length(); } ``` 在 Kotlin 中,判断过类型的变量会被**自动转换**: ```Kotlin if (obj is String) { obj.length // obj 在这里已经是 String 了 } ``` 空判断也一样: ```Kotlin val name: String? = getName() if (name != null) { name.length // 这里 name 已经是 String,而不是 String? } ``` ## 数据类 在 Java 中,写一个合格的实体类需要: 字段、构造函数、Getter/Setter、`equals`、`hashCode`、`toString`…… 少说也要几十行,于是有了 Lombok。 在 Kotlin 中: ```Kotlin data class User(val name: String, val age: Int) ``` 一行搞定。 编译器会自动生成: - `equals()` / `hashCode()` - `toString()`,输出形如 `User(name=Tom, age=18)` - `copy()`,用来复制并修改部分属性 - `component1()`、`component2()`…… ```Kotlin val tom = User("Tom", 18) val older = tom.copy(age = 19) val (name, age) = tom // 解构 ``` 还记得上面操作符表里的 `componentN` 吗? 解构其实就是在调用 `component1()`、`component2()`。 备注:数据类的一切都只针对于主构造函数中的 属性 成立。 Java 16 的 `record` 也做了类似的事。 但 `record` 没有 `copy`,也不支持解构。 虽然 Java 21 给 Java 的 `record` 带来了一个类似解构的能力,但他好奇怪啊。 非常的,拧巴。 ## 密封类 假设一个请求有三种状态:成功、失败、加载中。 ```Kotlin sealed interface Result { data class Success(val data: String) : Result data class Error(val e: Throwable) : Result data object Loading : Result } ``` `sealed` 的意思是:它的子类型在编译期就已经全部确定了。 所以搭配 `when` 使用时,编译器能检查你是否处理了所有情况: ```Kotlin fun render(result: Result): String = when (result) { is Result.Success -> result.data // 智能转换 is Result.Error -> "出错了:${result.e.message}" Result.Loading -> "加载中..." // 不需要 else } ``` 如果哪天新增了一个子类型,所有没处理它的 `when` 都会**直接编译报错**。 这比在运行期掉进 `default` 分支要可靠得多。 备注:sealed 并不能保证在运行时你的类不会出现更多的子类。 ## 函数是一等公民 ### 顶层函数 在 Java 中,所有函数都必须写在类里。 于是满世界都是 `XxxUtils`。 Kotlin 允许把函数直接写在文件顶层: ```Kotlin // StringExt.kt fun isEmail(str: String): Boolean = ... ``` 编译到 Java 后,其实还是放进了一个类,名字是“文件名 + Kt”: ```Java public final class StringExtKt { public static final boolean isEmail(String str) { ... } } ``` ### Lambda 与高阶函数 Kotlin 中函数也是一种类型,比如 `(Int) -> Int`。 它可以作为参数传入,也可以作为返回值。 如果函数的最后一个参数是 Lambda,就可以把它挪到括号外面: ```Kotlin val result = list.filter { it > 0 }.map { it * 2 } ``` 对比 Java Stream: ```Java List<Integer> result = list.stream() .filter(x -> x > 0) .map(x -> x * 2) .collect(Collectors.toList()); ``` ### inline 与 reified 普通的高阶函数,每次传入 Lambda 都会创建一个函数对象,有一定开销。 (实际上在 JVM 中,经过一定程度的优化后很多短期状态并不一定会真的有开销,但这是 JVM 优化的功劳) 用 `inline` 修饰后,编译器会把函数体连同 Lambda **直接复制到调用处**。 不会创建对象,性能和手写循环一样。 `inline` 还带来了一个 Java 做不到的能力:**实化泛型** (`reified`)。 Java 的泛型在运行期会被擦除,所以经常要这样写: ```Java <T> T fromJson(String json, Class<T> clazz) gson.fromJson(json, User.class); ``` 而 Kotlin 因为函数体被内联到了调用处,调用处的类型是确定的: ```Kotlin inline fun <reified T> Gson.fromJson(json: String): T = fromJson(json, T::class.java) val user: User = gson.fromJson(json) ``` ## 作用域函数 `let`、`run`、`apply`、`also` 是 Kotlin 中使用频率极高的五个函数。 | 函数 | 对象引用 | 返回值 | |---|---|---| | `let` | `it` | Lambda 结果 | | `run` | `this` | Lambda 结果 | | `apply` | `this` | 对象本身 | | `also` | `it` | 对象本身 | ```Kotlin val paint = Paint().apply { color = Color.RED strokeWidth = 4f } user?.let { println(it.name) } ``` 它们也不是什么语法。 只是标准库中定义的、带有接收者 Lambda 的 **inline 扩展函数**。 比如 `apply` 简化后就是: ```Kotlin inline fun <T> T.apply(block: T.() -> Unit): T { block() return this } ``` 扩展函数 + Lambda + inline,三个特性组合起来就有了这种写法。 具体可以参考 [DSL 那篇文章][1]。 ## 委托 ### 类委托 想给一个 `List` 加上日志功能。 在 Java 中,你得实现 `List` 接口的所有方法,再一个个转发给内部的 List。 在 Kotlin 中,用 `by` 就行: ```Kotlin class LoggingList<T>( private val inner: MutableList<T> ) : MutableList<T> by inner { override fun add(element: T): Boolean { println("add: $element") return inner.add(element) } } ``` 只需要重写你关心的方法,其余方法由编译器自动生成转发代码。 “组合优于继承”,Kotlin 帮你把样板代码写好了。 ### 属性委托 还记得开头说的吗?**属性实际上是一组 Getter/Setter。** 既然是 Getter/Setter,就可以把它们“委托”给另一个对象: ```Kotlin val config: Config by lazy { loadConfig() // 第一次访问时才执行,且只执行一次 } var name: String by Delegates.observable("") { _, old, new -> println("$old -> $new") } ``` 编译到 Java 大致是: ```Java private final Lazy config$delegate = LazyKt.lazy(...); public final Config getConfig() { return (Config) this.config$delegate.getValue(); } ``` 委托对象只需要提供 `getValue` / `setValue` 两个**操作符函数**。 属性、操作符、委托,在这里串起来了。 ## 协程 最后简单提一下协程。 在 Java 中,异步代码往往意味着回调嵌套: ```Java login(token -> { fetchUser(token, user -> { showUser(user); }); }); ``` 在 Kotlin 中,用 `suspend` 函数可以把异步代码写成同步的样子: ```Kotlin suspend fun loadUser() { val token = login() val user = fetchUser(token) showUser(user) } ``` 编译器会为 `suspend` 函数额外添加一个 `Continuation` 参数,并把函数体改写成状态机: ```Java public static final Object loadUser(Continuation<? super Unit> $completion) ``` 本质上,编译器替你把回调写好了。 然后,协程库提供了这些工具函数和默认实现。 # 总结 回头看这些特性,你会发现一个规律: - 属性 → Getter/Setter - 扩展函数/属性 → 静态函数 + 参数映射 - 操作符 → 约定名字的函数 - 数据类 → 编译器生成的 `equals`/`hashCode`/`copy`…… - 委托 → 编译器生成的转发代码 - 协程 → 编译器生成的状态机 Kotlin 的“丝滑”并没有用到运行时魔法。 它是**编译器替你写掉了本该由你手写的那些 Java 代码**。 也正因如此,Kotlin 与 Java 之间可以做到近乎 100% 的互操作。 你可以在一个 Java 项目里,从一个文件开始,慢慢尝试 Kotlin。 试过之后,大概就很难回去了。 [1]: /archives/3/ 最后修改:2026 年 11 月 08 日 © 允许规范转载 赞 1 如果觉得我的文章对你有用,请随意赞赏