跳到主要内容

Kotlin 标准库 UUID 实战:从 java.util.UUID 到 kotlin.uuid.Uuid 的跨平台迁移指南

· 阅读需 6 分钟
重庆 Kotlin User Group

在几乎所有像样的项目中,你都会遇到 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 类型能显著减少平台适配层的维护负担。