Kotlin 标准库 UUID 实战:从 java.util.UUID 到 kotlin.uuid.Uuid 的跨平台迁移指南
在几乎所有像样的项目中,你都会遇到 UUID——数据库主键、会话标识、设备指纹、分布式追踪 ID,处处都有它的身影。长期以来,Kotlin 开发者在 JVM 上直接用 java.util.UUID,在 iOS 上用 NSUUID,在 JS 上用 crypto.randomUUID()。三种类型、三套 API、三种序列化方式——跨平台项目中还得写适配层来互相转换。
Kotlin 2.4 终于结束了这个局面:kotlin.uuid.Uuid 正式从实验性 API 毕业,成为标准库的稳定成员。
从实验到稳定:UUID 的演进时间线
kotlin.uuid.Uuid 的诞生并非一蹴而就,它经历了三个关键阶段:
| 版本 | 状态 | 关键变化 |
|---|---|---|
| 2.0.20 | 实验性引入 | 首次提供 Uuid 类型,支持生成、解析、格式化 |
| 2.1.20 | 实验性增强 | 新增 Comparable 支持、parseHexDash()、toHexDashString() |
| 2.4.0 | 正式稳定 | 类型本身稳定,V4/V7 生成函数仍需 opt-in |
注意一个细节:类型稳定了,但生成函数还没有。Uuid.V4() 和 Uuid.V7() 仍然标注为 @ExperimentalStdlibApi,因为 V7 的字节布局依赖平台时钟精度,不同平台在时钟回拨时的行为存在差异,JetBrains 选择先稳定 API 设计,把平台适配的细节留给后续版本。
核心 API 速览
1. 生成 UUID
import kotlin.uuid.*
// V4(随机)UUID —— 稳定可用
val randomId: Uuid = Uuid.V4()
// V7(时间有序)UUID —— 仍需 opt-in
@OptIn(ExperimentalStdlibApi::class)
val timeOrderedId: Uuid = Uuid.V7()
V4 基于随机数生成,适合作为数据库主键和一般标识符。V7 基于时间戳生成,天然有序,适合需要按时间排序的场景(例如分布式日志追踪)。
2. 解析与格式化
// 从标准格式解析(hex-and-dash)
val uuid1 = Uuid.parse("550e8400-e29b-41d4-a716-446655440000")
// 从纯十六进制格式解析(2.1.20 起支持)
val uuid2 = Uuid.parse("550e8400e29b41d4a716446655440000")
// 也可以用语义更明确的 API
val uuid3 = Uuid.parseHexDash("550e8400-e29b-41d4-a716-446655440000")
val uuid4 = Uuid.parseHex("550e8400e29b41d4a716446655440000")
// 格式化输出
println(uuid1.toString()) // 550e8400-e29b-41d4-a716-446655440000
println(uuid1.toHexDashString()) // 550e8400-e29b-41d4-a716-446655440000
println(uuid1.toHexString()) // 550e8400e29b41d4a716446655440000
parse() 对无效输入返回 null 而非抛异常——这比 java.util.UUID.fromString() 的行为友好得多,后者在格式错误时直接抛 IllegalArgumentException。
3. 从 128 位值创建
// 从字节数组创建
val bytes = byteArrayOf(
0x55, 0x0E, 0x84.toByte(), 0x00,
0xE2.toByte(), 0x9B.toByte(), 0x41, 0xD4.toByte(),
0xA7.toByte(), 0x16, 0x44, 0x66,
0x55, 0x44, 0x00, 0x00
)
val uuid = Uuid.fromByteArray(bytes)
// 从两个 Long 值创建
val uuid2 = Uuid.fromULongs(
0x550E8400E29B41D4uL,
0xA716446655440000uL
)
4. 比较与排序
从 Kotlin 2.1.20 起,Uuid 实现了 Comparable<Uuid>,可以直接比较和排序:
val ids = listOf(
Uuid.parse("780e8400-e29b-41d4-a716-446655440005")!!,
Uuid.parse("550e8400-e29b-41d4-a716-446655440000")!!,
Uuid.parse("5ab88400-e29b-41d4-a716-446655440076")!!,
)
val sorted = ids.sorted()
// [550e8400-..., 5ab88400-..., 780e8400-...]
// 也可以直接用比较运算符
val a = Uuid.parse("550e8400-e29b-41d4-a716-446655440000")!!
val b = Uuid.parse("780e8400-e29b-41d4-a716-446655440005")!!
println(a < b) // true
实战:从 java.util.UUID 迁移
迁移前:典型的多平台痛点
// commonMain —— 需要自己定义 expect
expect fun randomUUID(): String
// jvmMain
actual fun randomUUID(): String = java.util.UUID.randomUUID().toString()
// iosMain
actual fun randomUUID(): String = NSUUID().UUIDString()
// jsMain
actual fun randomUUID(): String = crypto.randomUUID().toString()
每个平台返回的都是 String,类型安全?不存在的。你在 commonMain 里无法对 UUID 做任何有意义的操作——比较、解析、格式化全都得自己实现或依赖平台 API。
迁移后:统一的标准库方案
// commonMain —— 就这么简单
import kotlin.uuid.*
fun createUserId(): Uuid = Uuid.V4()
fun parseUserId(input: String): Uuid? = Uuid.parse(input)
// 在 commonMain 里就能做比较、排序、格式化
fun demo() {
val id1 = createUserId()
val id2 = createUserId()
println(id1 < id2) // 比较
println(id1.toHexDashString()) // 格式化
println(listOf(id1, id2).sorted()) // 排序
}
零 expect/actual,零第三方依赖,零类型转换。 这就是标准库统一 UUID 类型的价值。
与 Java UUID 互操作
迁移过程中不可避免需要与 Java API 交互。Kotlin 2.4 提供了直接的转换能力:
import kotlin.uuid.*
import java.util.UUID
// java.util.UUID → kotlin.uuid.Uuid
val javaUuid = UUID.randomUUID()
val kotlinUuid: Uuid = Uuid.fromJavaUuid(javaUuid)
// kotlin.uuid.Uuid → java.util.UUID
val backToJava: UUID = kotlinUuid.toJavaUuid()
这让渐进式迁移成为可能——你可以逐模块替换,不需要一次性改完所有代码。
在数据类中使用 UUID
一个常见的实际场景是在数据模型中使用 UUID 作为标识符:
import kotlin.uuid.*
import kotlinx.serialization.*
@Serializable
data class User(
val id: Uuid,
val name: String,
val email: String,
)
// 配合 kotlinx.serialization,Uuid 可以直接序列化/反序列化
fun main() {
val user = User(
id = Uuid.V4(),
name = "Alice",
email = "alice@example.com"
)
val json = Json.encodeToString(user)
// {"id":"550e8400-e29b-41d4-a716-446655440000","name":"Alice","email":"alice@example.com"}
val parsed = Json.decodeFromString<User>(json)
println(parsed.id == user.id) // true
}
选择 V4 还是 V7?
| 特性 | V4(随机) | V7(时间有序) |
|---|---|---|
| 生成方式 | 随机数 | 时间戳 + 随机数 |
| 有序性 | 无序 | 按时间自然有序 |
| 适用场景 | 数据库主键、一般标识符 | 分布式日志、事件溯源 |
| 碰撞概率 | 极低(2^122 空间) | 极低 |
| 稳定状态 | 已稳定 | 仍需 opt-in |
| 性能 | 快 | 略慢(需读取时钟) |
经验法则:除非你需要按时间排序(例如事件流、日志追踪),否则默认用 V4。V7 目前仍需 @OptIn,在生产代码中使用前请评估 opt-in 的维护成本。
全局 opt-in 配置
如果你的项目大量使用 V4/V7 生成函数,可以在模块级别统一 opt-in,避免到处写注解:
// build.gradle.kts
kotlin {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.uuid.ExperimentalUuidApi")
}
}
这样整个模块内的 Uuid.V4() 和 Uuid.V7() 调用都不再需要逐行标注。
总结
Kotlin 2.4 将 kotlin.uuid.Uuid 带入稳定阶段,解决了多平台项目中 UUID 类型碎片化的长期痛点。核心收益:
- 统一类型:
commonMain中直接使用,无需expect/actual - 类型安全:不再是裸
String,编译器帮你捕获错误 - 跨平台一致:JVM、iOS、JS、Wasm 共享同一套 API
- 渐进迁移:提供
fromJavaUuid()/toJavaUuid()互操作桥接
如果你的项目还在用 java.util.UUID 或自定义的 expect/actual 方案,现在是迁移到标准库的好时机——尤其是 Kotlin Multiplatform 项目,统一 UUID 类型能显著减少平台适配层的维护负担。