Kotlin 与 Java,不是简单的高低之分

很多人比较 Kotlin 和 Java 时,喜欢讨论一个问题:谁更先进,谁更好。

但如果真正深入使用这两门语言,这哥俩其实不是一个简单的"高低之分"。

Java 和 Kotlin 都在不断演进,只是它们在语言设计和代码表达上有不同的取向。

Kotlin 更积极地把可空性、函数类型、协程、Flow 等现代抽象融入语言和生态;Java 则更加重视显式、稳定、兼容性以及渐进式演进。

刚开始学习 Kotlin 时,我们常常把注意力放在语法差异上。

比如:

kotlin 复制代码
val name = "Tom"

对应 Java:

java 复制代码
String name = "Tom";

然后我们继续学习 val / var、空安全、data class、Lambda、扩展函数、协程、Flow 和 Compose。

这个时候我们会觉得:

Kotlin 不就是一门更简洁的 Java 吗?

如果只是比较代码量,确实很容易得出这个结论。

但 Kotlin 真正有价值的地方,并不是让 Java 代码少写几行。

而是它在一点点改变我们表达问题、组织状态和设计代码的方式

val 开始,我们减少可变状态;

data class,我们直接描述数据;

通过高阶函数和扩展函数,我们开始直接表达行为和领域语义;

再到协程和 Flow,我们开始用更自然的方式组织异步任务和持续变化的数据;

最终在 Compose 中:

text 复制代码
UI = f(State)

UI 甚至变成了状态的一种映射。

所以,与其说 Kotlin 是"更简洁的 Java",不如说:

Kotlin 是一门更强调表达力的语言。


1. val:先减少不必要的状态

最容易接触到的 Kotlin 特性,就是:

kotlin 复制代码
val name = "Tom"

var age = 18
age = 19

val 不能重新赋值,var 可以。

它们背后的思想:

能不变,就不要让它变化。

例如:

kotlin 复制代码
val userName = "Tom"

看到这里,我们可以很确定:

userName 这个引用建立之后,不会再指向其他对象。

而:

kotlin 复制代码
var userName = "Tom"
userName = "Jerry"

则意味着:

这个状态未来可能发生变化。

状态越少,程序需要考虑的情况通常就越少。

这也是为什么现代 Kotlin 代码中,我们经常看到:

kotlin 复制代码
val user = ...
val result = ...
val state = ...

而不是到处使用 var

这也是 Kotlin 后面很多设计思想的起点:

text 复制代码
val
 ↓
减少可变状态
 ↓
减少程序需要维护的状态变化

2. 空安全:把一部分运行时风险提前到编译期

Java 中非常经典的问题就是:

java 复制代码
String name = null;

System.out.println(name.length());

代码可以编译,但运行时可能出现:

text 复制代码
NullPointerException

Kotlin 则把可空性直接放进了类型系统:

kotlin 复制代码
val name: String? = null

这里的:

kotlin 复制代码
String

和:

kotlin 复制代码
String?

不是同一种语义。

String 表示:

我承诺这里一定有一个 String。

String? 表示:

这个值可能为空。

因此下面的代码不能直接通过:

kotlin 复制代码
val length = name.length

你必须显式处理:

kotlin 复制代码
val length = name?.length ?: 0

或者进行判空:

kotlin 复制代码
if (name != null) {
    println(name.length)
}

真正重要的不是 ?.?: 这些语法。

而是:

Kotlin 把"可能为空"从一种运行时约定,提升成了编译器可以参与检查的类型约束。

当然,这并不意味着 Kotlin 可以彻底消灭 NPE。

Java 互操作、平台类型、!!,以及某些反射式框架,都可能绕过 Kotlin 编译器的保护。

例如使用 Gson 反射解析数据时:

kotlin 复制代码
data class User(
    val name: String
)

如果服务端返回:

json 复制代码
{
  "name": null
}

某些情况下,运行时对象可能违背 Kotlin 源码中声明的非空约束。

因此网络数据这种"不可信的外部输入",应该在边界进行处理。

例如:

text 复制代码
Network DTO
    ↓
校验 / 默认值 / 转换
    ↓
Domain Model

也就是说:

类型系统可以帮助我们减少错误,但它不能替我们验证不可信的外部世界。


3. data class:直接描述数据

传统 Java 中,一个简单的数据对象往往需要大量样板代码:

  • 构造方法
  • Getter
  • Setter
  • equals
  • hashCode
  • toString

Kotlin 可以直接写成:

kotlin 复制代码
data class User(
    val name: String,
    val age: Int
)

这里其实已经表达了很多信息:

text 复制代码
User 是什么?
↓
它有哪些数据?
↓
这些数据是否允许重新赋值?

同时编译器还会提供常用能力:

kotlin 复制代码
val user = User("Tom", 18)

println(user)
println(user == User("Tom", 18))

val nextUser = user.copy(age = 19)

甚至可以直接解构:

kotlin 复制代码
val (name, age) = user

现代 Java 中的 record 也解决了类似的问题。

所以重点并不是:

"Kotlin 有 data class,Java 没有。"

而是:

Kotlin 很自然地把数据模型、属性、复制、比较和解构组合到了一起。

代码从:

"如何写一个符合规范的 JavaBean?"

变成:

"这个数据到底是什么?"

这就是 Kotlin 表达力提升的一个典型例子。


4. Lambda 和高阶函数:让"行为"也可以被表达

数据可以被抽象。

那么行为呢?

Kotlin 可以直接把函数作为数据一样传递:

kotlin 复制代码
fun execute(block: () -> Unit) {
    block()
}

execute {
    println("Hello")
}

函数可以:

  • 作为参数
  • 作为返回值
  • 被组合和复用

例如集合操作:

kotlin 复制代码
val names = users
    .filter { it.age >= 18 }
    .map { it.name }

代码表达的是:

找出成年人,然后取出他们的名字。

而不是:

创建一个循环,判断条件,把结果放到另一个集合里。

当然,Java 也已经支持 Lambda 和 Stream。

所以这里同样不是简单比较:

Kotlin 有,Java 没有。

真正的区别在于 Kotlin 把函数类型、高阶函数、Lambda、尾随 Lambda 等能力结合得非常自然。

于是"行为"本身也成为一种可以组合的抽象。

这也是后面很多 Kotlin API 的基础:

text 复制代码
集合操作
    ↓
高阶函数
    ↓
Flow
    ↓
Compose
    ↓
DSL

5. 扩展函数:让代码更接近领域语言

Java 中我们经常会看到工具类:

java 复制代码
OrderUtils.canBeShipped(order);

Kotlin 可以写成:

kotlin 复制代码
fun Order.canBeShipped(): Boolean {
    return paid && amount > 0
}

调用:

kotlin 复制代码
if (order.canBeShipped()) {
    ship(order)
}

乍一看只是少写了一些字符。

但真正的变化是表达方式。

text 复制代码
OrderUtils.canBeShipped(order)

更像是在:

调用一个工具。

而:

text 复制代码
order.canBeShipped()

更像是在:

向 Order 提出一个问题。

这会让代码越来越接近业务语言。

例如:

kotlin 复制代码
order.canBeShipped()
user.isVip()
account.isExpired()
payment.isSuccessful()

代码读起来越来越像业务规则。

当然,扩展函数并没有真正修改原来的类。

它本质上仍然是静态分发:

  • 不能访问原类的私有成员
  • 不能真正给原类增加字段
  • 不能覆盖原类成员函数

所以扩展函数解决的不是:

"如何修改一个类?"

而是:

"如何让代码更自然地表达这个操作属于谁?"


6. 协程:解决的不是线程,而是异步任务如何组织

到了协程,Kotlin 开始真正影响我们设计程序的方式。

传统 Android 异步代码经常需要同时考虑:

text 复制代码
线程
线程池
回调
线程切换
取消
生命周期
异常

代码很容易变成:

text 复制代码
请求
 ↓
回调
 ↓
切线程
 ↓
回调
 ↓
判断生命周期
 ↓
异常处理

协程则允许我们把业务逻辑重新写成接近同步代码的形式:

kotlin 复制代码
viewModelScope.launch {
    val user = repository.getUser()
    showUser(user)
}

但这里有一个非常重要的概念:

协程不是线程。

可以把三者理解成:

text 复制代码
Coroutine
    ↓
任务如何组织
    ↓
Dispatcher
    ↓
任务如何调度
    ↓
Thread
    ↓
最终执行资源

也就是说:

线程解决的是"在哪里执行",协程解决的是"任务之间如何组织"。

例如:

kotlin 复制代码
suspend fun getUser(): User =
    withContext(Dispatchers.IO) {
        api.getUser()
    }

Dispatchers.IO 描述的是一种执行策略,而不是"协程本身"。

协程真正重要的能力之一,是结构化并发

例如:

kotlin 复制代码
viewModelScope.launch {
    val user = repository.getUser()
    ...
}

这个任务属于 viewModelScope

当 ViewModel 被销毁时,相关任务也可以被取消。

这意味着:

text 复制代码
任务
 ↓
有明确的父级
 ↓
生命周期明确
 ↓
取消可以传播
 ↓
异常可以传播

所以协程解决的远远不只是:

"回调太难看。"

它真正解决的是:

异步任务之间应该如何建立关系,以及如何管理它们的生命周期、取消和异常。


7. Flow:从"一次结果"走向"持续变化"

如果协程解决的是异步任务的组织,那么 Flow 进一步解决的是:

数据不断变化怎么办?

一次网络请求通常是:

text 复制代码
请求
 ↓
结果

回调非常适合这种场景:

text 复制代码
请求完成 → 通知我

但很多 Android 数据并不是一次性的。

例如:

  • 数据库数据
  • 登录状态
  • 网络状态
  • 播放进度
  • 用户状态
  • UI 状态

它们更像:

text 复制代码
状态 A
 ↓
状态 B
 ↓
状态 C
 ↓
状态 D

Flow 就非常适合表达这种持续变化的数据:

kotlin 复制代码
repository.observeUser()
    .filter { it.isValid }
    .map { it.name }
    .distinctUntilChanged()
    .collect(::updateUi)

它表达的是:

当用户数据发生变化时,只处理有效数据,取出名称,并且只有名称真正发生变化时才更新 UI。

于是程序的思考方式开始发生变化。

过去我们可能会问:

"什么时候调用刷新 UI 的方法?"

现在更容易问:

"当前状态是什么?状态会如何变化?"

这会自然形成一条数据链路:

text 复制代码
Repository
    ↓
Flow
    ↓
ViewModel
    ↓
StateFlow
    ↓
UI

这也是现代 Android 架构非常重要的一种思维方式:

UI 不应该到处主动拉取数据,而应该观察状态。


8. Compose:UI 变成状态的映射

到了 Compose,这条线终于完整闭环。

传统 View 开发经常是命令式的:

kotlin 复制代码
textView.text = name
textView.visibility = View.VISIBLE

我们告诉 UI:

你现在做什么。

Compose 更接近声明式:

kotlin 复制代码
@Composable
fun UserName(name: String) {
    Text(text = name)
}

它表达的是:

当前状态下,UI 应该长什么样。

因此可以把它抽象成:

text 复制代码
UI = f(State)

状态发生变化:

text 复制代码
State A
   ↓
State B

Compose 根据新的状态重新计算需要呈现的 UI。

这时候再回头看前面的 Kotlin 特性,会发现它们其实并不是孤立存在的。

text 复制代码
val
 ↓
减少可变状态

data class
 ↓
描述状态

Flow / StateFlow
 ↓
传递状态变化

Compose
 ↓
状态映射 UI

这是一条非常完整的设计思路。


Kotlin 真正改变的是什么?

把前面的内容放在一起:

text 复制代码
val
 ↓
减少可变状态

data class
 ↓
直接表达数据

高阶函数
 ↓
直接表达行为

扩展函数
 ↓
直接表达领域语义

协程
 ↓
组织异步任务

Flow
 ↓
表达数据变化

Compose
 ↓
状态映射 UI

Kotlin其实都在做同一件事:

减少不必要的实现细节,让代码越来越接近我们真正要解决的问题。

所以本文并不是想证明 Kotlin 比 Java 更高级,而是Kotlin 到底改变了我们哪些编程习惯, 完。

相关推荐
一笑的小酒馆8 小时前
Android12Launcher3实现应用裁剪
android
FinelyYang13 小时前
UniApp + LiveKit:Android 端为什么要用原生做屏幕共享,以及我们怎么落地的
android·uni-app
蓝速科技13 小时前
蓝速科技信创落地:平衡安全合规与项目成本的实战方案
android·运维·人工智能·科技·安全·电脑·鸿蒙
又见情义16 小时前
RK3568 Android 13 SDK 开机自动开启共享热点实践
android
mmsx16 小时前
return false 不是拦截:一次按键触发两个动作的事件冒泡陷阱
android·bug
DU_YULIN17 小时前
Camera HAL ThreadManager概述
android·camera hal
淡淡的香烟18 小时前
Android中的MQTT通信原理解析
android·物联网
淡淡的香烟18 小时前
Android Camera开发详解
android·音视频
mmsx19 小时前
基于 Android 的校园信息管理系统源码
android·java·okhttp