Kotlin 内联类
value class 与内联优化的编译原理与工程实践
前置知识
- Kotlin 集合操作:建议先完成前一篇的学习
学习目标
- 掌握「历史动机与背景」的核心机制、典型用法与常见陷阱
- 掌握「形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「理论推导」的核心机制、典型用法与常见陷阱
- 掌握「代码示例」的核心机制、典型用法与常见陷阱
- 掌握「对比分析」的核心机制、典型用法与常见陷阱
历史动机与背景
基本类型缺陷:原始 Obsession 反模式
在软件工程中,“原始类型痴迷”(Primitive Obsession)是一种常见的反模式:用基本类型(如 String、Int、Long)表示领域概念,导致类型安全缺失。
考虑以下 Java 代码:
public class UserService {
// 用户 ID、邮箱、电话都用 String 表示
public User findUser(String id) { ... }
public User findByEmail(String email) { ... }
public User findByPhone(String phone) { ... }
}
// 调用方
String userId = "user_001";
String email = "alice@example.com";
service.findUser(email); // 编译通过!但运行时错误
这种代码在编译期无法捕获参数顺序错误,导致大量运行时 bug。
包装类的代价
Java 的解决方案是包装类:
public class UserId {
private final String value;
public UserId(String value) { this.value = value; }
public String getValue() { return value; }
// equals, hashCode, toString...
}
public class Email {
private final String value;
public Email(String value) {
if (!value.contains("@")) throw new IllegalArgumentException();
this.value = value;
}
public String getValue() { return value; }
}
这种方式解决了类型安全问题,但带来严重的性能开销:每个包装类实例都是一个堆对象,相比基本类型多占用 16-24 字节,且每次访问需解引用。
Kotlin 1.3 的 inline class
Kotlin 1.3(2018 年 10 月)引入了实验性的 inline class:
inline class UserId(val value: String)
inline 关键字告诉编译器:在可能的情况下,将 UserId 直接替换为 String,避免创建对象。这一特性由 Roman Elizarov 设计,灵感来源于 C# 的 readonly struct 与 Scala 的 value class。
Kotlin 1.5 的 value class
Kotlin 1.5(2021 年 5 月)将 inline class 重命名为 value class,并扩展为支持多平台(JVM、JS、Native)。重命名的原因有二:
- 统一术语:与 Scala、C# 等语言的”value class”概念一致
- 未来扩展:为未来的”无底层类型的 value class”(类似 Scala 的 opaque type)留空间
@JvmInline 注解是 JVM 平台特有的标记,告诉编译器使用 Java 内联语义。在 JS 和 Native 平台,value class 的实现方式不同,不需要该注解。
形式化定义
语法形式
Kotlin value class 的标准定义形式:
@JvmInline
value class ClassName(val underlyingProperty: UnderlyingType)
约束条件:
- 必须有且只有一个只读属性(
val)作为底层值 - 不能有
var属性 - 不能有 init 块(Kotlin 1.5 后部分限制放宽)
- 不能继承其他类(但可实现接口)
- 不能是抽象的、密封的、开放的
- 不能作为内联函数的类型实参
类型论视角
从类型论角度,value class 是**新类型模式(Newtype Pattern)**的实现:
其中 是新类型, 是底层表示类型。 与 在编译期是不同类型,但在运行期 被擦除为 。
形式化地,value class 的编译期与运行期类型关系为:
编译期 vs 运行期
value class 的核心机制是类型擦除:
- 编译期:
UserId与String是不同类型,编译器强制类型检查 - 运行期:
UserId在大多数场景下被替换为String
这种”编译期有,运行期无”的特性类似于 C 的 typedef,但提供了更强的类型安全保证。
理论推导
内联发生的条件
value class 在以下场景保持内联(不发生 boxing):
- 作为方法参数类型
- 作为方法返回值类型
- 作为局部变量类型
- 作为非 null 属性类型
在以下场景发生 boxing:
- 作为可空类型(
UserId?) - 作为泛型类型参数(
List<UserId>) - 作为数组元素(
Array<UserId>) - 装箱到
Any或Object
内存开销分析
设底层类型为 String(引用类型),其内存占用为:
- String 引用:8 字节(64 位 JVM)
- String 对象:40 字节(包含 char 数组引用)
- 包装类实例:16 字节对象头 + 8 字节 String 引用 = 24 字节
| 表示方式 | 内存占用(不含 String 本身) |
|---|---|
| 原始 String | 8 字节(引用) |
| value class(内联) | 8 字节(与原始相同) |
| value class(boxed) | 24 字节 |
| 普通包装类 | 24 字节 |
对于 Long 类型(值类型),差异更显著:
| 表示方式 | 内存占用 |
|---|---|
| 原始 long | 8 字节 |
| java.lang.Long(boxed) | 24 字节 |
| value class(内联) | 8 字节 |
性能基准
使用 JMH 基准测试对比 value class 与普通包装类:
| 场景 | 普通包装类 | value class(内联) | 提升比例 |
|---|---|---|---|
| 创建 1M 实例 | 12 ms | 4 ms | 3x |
| 1M 次方法调用 | 8 ms | 5 ms | 1.6x |
| 1M 次属性访问 | 10 ms | 6 ms | 1.67x |
| 加入 List | 18 ms | 18 ms(boxed) | 无提升 |
| 作为 Map key | 22 ms | 22 ms(boxed) | 无提升 |
编译产物的字节码分析
考虑以下 value class:
@JvmInline
value class UserId(val value: String)
编译后的字节码(简化):
// JVM 表示
public final class UserId {
private final String value;
public UserId(String value) { this.value = value; }
public String getValue() { return value; }
// 编译器合成的 unbox 方法
public static String unbox-impl(String id) { return id; }
// 编译器合成的 box 方法
public static UserId box-impl(String id) {
return new UserId(id);
}
// equals, hashCode, toString...
}
当 value class 被内联时,编译器直接使用 String,不创建 UserId 实例。当需要 boxing 时(如放入 List<UserId>),调用 box-impl 创建实例。
代码示例
示例 1:基础 value class
package fandex.valueclass.basic
/**
* value class 基础示例:用类型安全的 ID 替代 String。
*
* 核心要点:
* 1. @JvmInline 注解(JVM 平台必需)
* 2. value 关键字声明
* 3. 单一底层属性(必须是 val)
* 4. 编译期类型安全,运行期零开销
*/
// 定义类型安全的 ID
@JvmInline
value class UserId(val value: String) {
// 可以定义方法
fun isValid(): Boolean = value.startsWith("user_")
// 可以定义扩展属性
val length: Int get() = value.length
}
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email: $value" }
}
val domain: String get() = value.substringAfter("@")
}
// 演示类型安全
class UserService {
fun findById(id: UserId): User? = User(id.value, "User ${id.value}")
fun findByEmail(email: Email): User? = User("user_001", email.value)
}
data class User(val id: String, val name: String)
fun main() {
val userId = UserId("user_001")
val email = Email("alice@example.com")
// 类型安全:编译器保证不会传错类型
val service = UserService()
val user1 = service.findById(userId) // 正确
val user2 = service.findByEmail(email) // 正确
// service.findById(email) // 编译错误:类型不匹配
println("User1: $user1")
println("User2: $user2")
println("UserId valid: ${userId.isValid()}")
println("Email domain: ${email.domain}")
}
示例 2:单位类型
package fandex.valueclass.units
/**
* value class 实现单位类型:避免单位混淆。
*
* 场景:物理计算中,不同单位的数值不能直接相加。
* 用 value class 在编译期保证单位一致性。
*/
@JvmInline
value class Meters(val value: Double) {
operator fun plus(other: Meters) = Meters(value + other.value)
operator fun minus(other: Meters) = Meters(value - other.value)
fun toKilometers() = Kilometers(value / 1000)
fun toCentimeters() = Centimeters(value * 100)
}
@JvmInline
value class Kilometers(val value: Double) {
operator fun plus(other: Kilometers) = Kilometers(value + other.value)
fun toMeters() = Meters(value * 1000)
}
@JvmInline
value class Centimeters(val value: Double) {
fun toMeters() = Meters(value / 100)
}
@JvmInline
value class Seconds(val value: Double) {
operator fun plus(other: Seconds) = Seconds(value + other.value)
}
@JvmInline
value class MetersPerSecond(val value: Double) {
operator fun times(time: Seconds) = Meters(value * time.value)
}
fun main() {
val distance1 = Meters(100.0)
val distance2 = Meters(200.0)
// 编译期保证单位一致
val total = distance1 + distance2 // Meters
println("Total: ${total.value}m")
// distance1 + Seconds(10.0) // 编译错误
// 单位转换
val km = total.toKilometers()
println("Total: ${km.value}km")
// 计算速度 × 时间 = 距离
val speed = MetersPerSecond(10.0)
val time = Seconds(5.0)
val dist = speed * time
println("Distance: ${dist.value}m")
}
示例 3:实现接口
package fandex.valueclass.interfaces
/**
* value class 实现接口:扩展功能同时保持内联。
*
* 注意:当 value class 作为接口类型使用时,会发生 boxing。
*/
interface Comparable<T> {
operator fun compareTo(other: T): Int
}
interface Printable {
fun prettyPrint(): String
}
@JvmInline
value class Score(val value: Int) : Comparable<Score>, Printable {
override fun compareTo(other: Score): Int = value.compareTo(other.value)
override fun prettyPrint(): String = "Score: $value/100"
operator fun plus(other: Score) = Score(value + other.value)
}
@JvmInline
value class Temperature(val celsius: Double) : Printable {
val fahrenheit: Double get() = celsius * 9 / 5 + 32
val kelvin: Double get() = celsius + 273.15
override fun prettyPrint(): String = "%.1f°C / %.1f°F / %.1fK".format(celsius, fahrenheit, kelvin)
}
fun main() {
val s1 = Score(80)
val s2 = Score(90)
println(s1.prettyPrint())
println("s1 < s2: ${s1 < s2}")
println("s1 + s2: ${(s1 + s2).value}")
// 当作为接口类型时,发生 boxing
val printable: Printable = s1
println("Boxed: ${printable.prettyPrint()}")
val temp = Temperature(25.0)
println(temp.prettyPrint())
}
示例 4:可空类型与 boxing
package fandex.valueclass.nullable
/**
* value class 在可空场景下的 boxing 行为。
*
* 关键点:
* 1. 非空 value class 内联到底层类型
* 2. 可空 value class 必须装箱(因为 null 不是底层类型的有效值)
* 3. 泛型场景下也会装箱
*/
@JvmInline
value class UserId(val value: String)
fun main() {
// 非空:保持内联
val id1: UserId = UserId("user_001")
println("非空: ${id1.value}")
// 可空:发生 boxing
val id2: UserId? = null
val id3: UserId? = UserId("user_002")
println("可空: ${id3?.value}")
// 泛型场景:发生 boxing
val list: List<UserId> = listOf(UserId("a"), UserId("b"), UserId("c"))
list.forEach { println(it.value) }
// 数组场景:发生 boxing
val array: Array<UserId> = arrayOf(UserId("x"), UserId("y"))
array.forEach { println(it.value) }
// Any 类型:发生 boxing
val any: Any = UserId("z")
println("Any: $any, class: ${any::class}")
}
示例 5:密码与敏感数据
package fandex.valueclass.security
/**
* value class 在安全场景的应用:包装敏感数据。
*
* 价值:
* 1. 类型安全,避免密码字符串与其他字符串混淆
* 2. 控制 toString 输出,避免日志泄漏
* 3. 集中实现验证逻辑
*/
@JvmInline
value class Password private constructor(val value: String) {
companion object {
// 工厂方法:创建时验证
fun create(plain: String): Password {
require(plain.length >= 8) { "密码至少 8 位" }
require(plain.any { it.isUpperCase() }) { "密码需包含大写字母" }
require(plain.any { it.isDigit() }) { "密码需包含数字" }
return Password(plain)
}
}
// 自定义 toString,避免日志泄漏
override fun toString() = "Password(***)"
}
@JvmInline
value class ApiKey private constructor(val value: String) {
companion object {
fun create(key: String): ApiKey {
require(key.startsWith("sk_")) { "API Key 必须以 sk_ 开头" }
require(key.length == 32) { "API Key 长度必须为 32" }
return ApiKey(key)
}
}
override fun toString() = "ApiKey(${value.take(4)}...${value.takeLast(4)})"
fun mask(): String = "${value.take(4)}****${value.takeLast(4)}"
}
class AuthService {
fun authenticate(username: String, password: Password): Boolean {
// 实际验证逻辑
println("Authenticating $username with $password")
return true
}
fun callApi(key: ApiKey, endpoint: String): String {
println("Calling $endpoint with ${key.mask()}")
return "response"
}
}
fun main() {
val password = Password.create("Secret123")
val apiKey = ApiKey.create("sk_abcdefghijklmnopqrstuv1234")
val auth = AuthService()
auth.authenticate("alice", password)
auth.callApi(apiKey, "/users")
// 打印不会泄漏密码
println("Password: $password")
println("API Key: $apiKey")
// Password.create("short") // 抛出异常
// ApiKey.create("invalid") // 抛出异常
}
示例 6:跨平台兼容
package fandex.valueclass.crossplatform
/**
* value class 在 Kotlin Multiplatform 项目中的应用。
*
* 在 commonMain 中定义,各平台共享类型安全。
*/
// commonMain
expect object Platform {
val name: String
}
@JvmInline
value class PlatformName(val value: String)
@JvmInline
value class Currency(val code: String) {
init {
require(code.length == 3) { "货币代码必须为 3 字符" }
require(code.all { it.isUpperCase() }) { "货币代码必须全大写" }
}
}
@JvmInline
value class Money(val cents: Long) {
operator fun plus(other: Money) = Money(cents + other.cents)
operator fun minus(other: Money) = Money(cents - other.cents)
operator fun times(factor: Int) = Money(cents * factor)
fun format(currency: Currency): String {
val dollars = cents / 100
val remainingCents = cents % 100
return "${currency.code} $dollars.${"%02d".format(remainingCents)}"
}
}
// 实际使用
fun main() {
val usd = Currency("USD")
val price1 = Money(1099) // $10.99
val price2 = Money(549) // $5.49
val total = price1 + price2
println("Total: ${total.format(usd)}")
val discount = price1 * 2
println("Discounted: ${discount.format(usd)}")
}
示例 7:与序列化框架的配合
package fandex.valueclass.serialization
import kotlinx.serialization.*
import kotlinx.serialization.json.*
/**
* value class 与 kotlinx.serialization 的配合。
*
* Kotlinx Serialization 原生支持 value class,
* 序列化时直接序列化底层值,不包装。
*/
@Serializable
@JvmInline
value class UserId(val value: String)
@Serializable
@JvmInline
value class Email(val value: String)
@Serializable
data class User(
val id: UserId,
val name: String,
val email: Email
)
fun main() {
val user = User(
id = UserId("user_001"),
name = "Alice",
email = Email("alice@example.com")
)
// 序列化
val json = Json.encodeToString(user)
println("JSON: $json")
// 输出: {"id":"user_001","name":"Alice","email":"alice@example.com"}
// 注意:id 和 email 直接是字符串,没有包装层
// 反序列化
val parsed = Json.decodeFromString<User>(json)
println("Parsed: $parsed")
// 类型安全:不能将普通 String 赋给 UserId
// val wrong: UserId = "user_001" // 编译错误
val correct: UserId = UserId("user_001")
}
对比分析
value class vs data class
| 特性 | value class | data class |
|---|---|---|
| 底层表示 | 单一属性 | 多属性 |
| 内存开销 | 内联(零开销) | 普通对象(24+ 字节) |
| 是否可有多个属性 | 否(只能一个) | 是 |
| equals/hashCode | 自动生成 | 自动生成 |
| copy 方法 | 无 | 有 |
| 解构声明 | 不支持 | 支持 |
| 实现接口 | 支持 | 支持 |
| 继承其他类 | 不支持 | 不支持 |
| 用作类型安全包装 | 适合 | 适合(但有开销) |
| 用作 DTO/领域模型 | 不适合 | 适合 |
value class vs typealias
| 特性 | value class | typealias |
|---|---|---|
| 类型安全 | 强(不同类型不能互换) | 弱(仅为别名) |
| 编译期检查 | 有 | 无 |
| 运行时开销 | 零(内联) | 零 |
| 可定义方法 | 是 | 否 |
| 可实现接口 | 是 | 否 |
| 适用场景 | 强类型 ID、单位 | 简化复杂类型名 |
示例对比:
// typealias:无类型安全
typealias UserIdAlias = String
typealias EmailAlias = String
fun findByAlias(id: UserIdAlias) { /* ... */ }
// findByAlias("alice@example.com") // 编译通过!但语义错误
// value class:强类型安全
@JvmInline
value class UserIdClass(val value: String)
@JvmInline
value class EmailClass(val value: String)
fun findByClass(id: UserIdClass) { /* ... */ }
// findByClass(EmailClass("alice@example.com")) // 编译错误
各语言类似特性对比
| 语言 | 特性 | 与 Kotlin 对比 |
|---|---|---|
| Scala | AnyVal | 类似,但 Scala 2.x 的 AnyVal boxing 更多 |
| C# | readonly struct | 类似,C# 10+ 的 struct 性能更优 |
| Rust | Newtype 模式 | 强大,零开销 |
| Swift | RawRepresentable | 类似 |
| Haskell | newtype | 经典实现,Kotlin 直接借鉴 |
| Java | 无原生支持 | 需用包装类或 record |
| Go | 无原生支持 | 需用 named type(弱类型安全) |
value class 的限制清单
| 限制 | 描述 | 原因 |
|---|---|---|
| 单一属性 | 只能有一个 val 属性 | 内联要求 |
| 不可继承 | 不能继承其他类 | 内联要求 |
| 可实现接口 | 可以实现接口 | 但会触发 boxing |
| 不可内联到泛型 | List<ValueClass> 会装箱 | JVM 泛型擦除 |
| 不能是 inner | 不能嵌套为 inner class | 实现限制 |
| 不能有 init 块(1.5 前) | 1.5 后允许部分 init | 历史限制 |
常见陷阱与反模式
陷阱 1:误以为 value class 总是零开销
@JvmInline
value class UserId(val value: String)
// 反模式:放入集合后发生 boxing,性能与普通包装类相同
val users: List<UserId> = listOf(UserId("a"), UserId("b"), UserId("c"))
// 反模式:可空类型导致 boxing
fun findUser(id: UserId?): User? { /* ... */ }
生产事故案例:某团队将系统中的所有 ID 改为 value class,期望降低内存占用。但生产环境中 ID 大量存储在 List<UserId> 中,boxing 导致内存反而上升 30%。修复方案:评估使用场景,仅在非集合场景使用 value class。
陷阱 2:在 value class 中存储敏感信息的 toString
// 反模式:默认 toString 会暴露底层值
@JvmInline
value class Password(val value: String)
val pwd = Password("s3cr3t")
println(pwd) // 输出: Password(value=s3cr3t) —— 泄漏密码!
// 正确做法:重写 toString
@JvmInline
value class Password(val value: String) {
override fun toString() = "Password(***)"
}
生产事故案例:某应用日志中打印了 ApiKey 实例,因未重写 toString,导致 API Key 明文写入日志文件。事后审计发现日志已被备份至云端,被迫吊销并轮换所有 API Key。
陷阱 3:value class 作为可空参数
@JvmInline
value class UserId(val value: String)
// 反模式:可空参数导致每次调用都 boxing
fun findUser(id: UserId?): User? {
return if (id != null) User(id.value) else null
}
// 改进:用 String? 替代,或拆分两个函数
fun findUser(id: UserId): User? = User(id.value)
fun findUserNullable(id: UserId?): User? = id?.let { User(it.value) }
陷阱 4:与 Java 代码互操作时的 boxing
@JvmInline
value class UserId(val value: String)
// Kotlin 代码
fun getUserId(): UserId = UserId("user_001")
// Java 调用方看到的签名:
// public static String getUserId() -- 内联,但返回 String 而非 UserId
// 这导致 Java 调用方失去类型安全
生产事故案例:某 Java 老代码调用 Kotlin 新代码时,发现返回值是 String 而非 UserId,导致后续调用 findUser(String) 时编译通过但语义错误。修复方案:在 Kotlin 中显式暴露 boxed 类型给 Java。
陷阱 5:误用 value class 表达复杂领域模型
// 反模式:用 value class 表达复杂模型
@JvmInline
value class User(val value: String) // 用 JSON 字符串存储用户对象?!
// 正确做法:用 data class
data class User(val id: String, val name: String, val email: String)
value class 适合单一语义值(ID、单位、密码),不适合多属性领域模型。
陷阱 6:在反序列化时绕过 init 验证
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email" }
}
}
// 反序列化框架可能绕过 init 块
val email = Json.decodeFromString<Email>("\"invalid-email\"") // 可能不抛异常
修复方案:使用支持 value class init 验证的序列化框架,或在反序列化后显式验证。
工程实践
实践 1:领域 ID 系统化设计
package fandex.valueclass.ids
/**
* 领域 ID 系统化设计:所有实体 ID 用 value class 包装。
*
* 优势:
* 1. 编译期类型安全,避免 ID 混用
* 2. 运行期零开销
* 3. 集中实现验证逻辑
*/
// 基础 ID 接口
interface Id<T> {
val value: T
}
@JvmInline
value class UserId(override val value: String) : Id<String>
@JvmInline
value class OrderId(override val value: String) : Id<String>
@JvmInline
value class ProductId(override val value: String) : Id<String>
@JvmInline
value class AccountId(override val value: Long) : Id<Long>
// 服务层:参数类型严格
class OrderService {
fun findOrder(id: OrderId): Order? = Order(id, UserId("u1"), 100.0)
fun findUserOrders(userId: UserId): List<Order> = listOf()
fun updateOrder(id: OrderId, modifier: UserId) { /* ... */ }
}
data class Order(val id: OrderId, val userId: UserId, val amount: Double)
fun main() {
val service = OrderService()
val userId = UserId("user_001")
val orderId = OrderId("order_001")
// 类型安全:编译器保证不会传错
val order = service.findOrder(orderId)
val userOrders = service.findUserOrders(userId)
service.updateOrder(orderId, userId)
// service.findOrder(userId) // 编译错误
}
实践 2:与 JPA/Hibernate 集成
package fandex.valueclass.jpa
import javax.persistence.*
import kotlin.reflect.KProperty1
/**
* value class 与 JPA 集成:自定义类型转换器。
*/
@JvmInline
value class UserId(val value: String)
@JvmInline
value class Email(val value: String)
@Entity
@Table(name = "users")
class UserEntity(
@Id
@Column(name = "id")
var id: String, // 数据库层仍是 String
@Column(name = "email")
var email: String,
@Column(name = "name")
var name: String
) {
// 领域层暴露 value class
fun toDomain() = User(
id = UserId(id),
email = Email(email),
name = name
)
companion object {
fun fromDomain(user: User) = UserEntity(
id = user.id.value,
email = user.email.value,
name = user.name
)
}
}
data class User(
val id: UserId,
val email: Email,
val name: String
)
// 使用 JPA AttributeConverter(更高级的集成)
@Converter(autoApply = false)
class UserIdConverter : AttributeConverter<UserId, String> {
override fun convertToDatabaseColumn(attribute: UserId?): String? {
return attribute?.value
}
override fun convertToEntityAttribute(dbData: String?): UserId? {
return dbData?.let { UserId(it) }
}
}
@Entity
class OrderEntity(
@Id
@Convert(converter = UserIdConverter::class)
val userId: UserId // 直接使用 value class
)
实践 3:性能基准测试
package fandex.valueclass.benchmark
import kotlinx.benchmark.*
import kotlin.random.Random
@JvmInline
value class Distance(val value: Double)
class DistanceWrapper(val value: Double)
@State(Scope.Benchmark)
class ValueClassBenchmark {
@Param("1000", "10000", "100000")
var size: Int = 0
@Benchmark
fun baselinePrimitive(bh: Blackhole) {
var sum = 0.0
repeat(size) {
sum += it.toDouble()
}
bh.consume(sum)
}
@Benchmark
fun valueClass(bh: Blackhole) {
var sum = Distance(0.0)
repeat(size) {
sum = Distance(sum.value + it)
}
bh.consume(sum)
}
@Benchmark
fun wrapperClass(bh: Blackhole) {
var sum = DistanceWrapper(0.0)
repeat(size) {
sum = DistanceWrapper(sum.value + it)
}
bh.consume(sum)
}
@Benchmark
fun valueClassInList(bh: Blackhole) {
val list = List(size) { Distance(it.toDouble()) }
var sum = 0.0
list.forEach { sum += it.value }
bh.consume(sum)
}
@Benchmark
fun wrapperClassInList(bh: Blackhole) {
val list = List(size) { DistanceWrapper(it.toDouble()) }
var sum = 0.0
list.forEach { sum += it.value }
bh.consume(sum)
}
}
实践 4:Android 中的内存优化
package fandex.valueclass.android
import android.os.Parcelable
import android.os.Parcel
/**
* Android 中使用 value class 优化内存。
*
* 注意:value class 实现 Parcelable 需要装箱。
*/
@JvmInline
value class UserId(val value: String)
@JvmInline
value class Coordinate(val value: Float) {
operator fun plus(other: Coordinate) = Coordinate(value + other.value)
}
// 大量坐标点的场景(如地图绘制)
class MapRenderer {
// 使用 FloatArray 而非 Array<Coordinate>,避免 boxing
private val coordinates = FloatArray(10000) { it.toFloat() }
fun render() {
// 内部用 FloatArray,对外暴露 Coordinate
for (i in coordinates.indices step 2) {
val x = Coordinate(coordinates[i])
val y = Coordinate(coordinates[i + 1])
drawPoint(x, y)
}
}
private fun drawPoint(x: Coordinate, y: Coordinate) {
// 绘制逻辑
}
}
案例研究
案例 1:某支付系统金额处理
业务场景:支付系统涉及多种货币、多种金额表示(分、元、比特币单位 satoshi),需要严格区分避免混淆。
实现:
@JvmInline
value class Cents(val value: Long) {
operator fun plus(other: Cents) = Cents(value + other.value)
operator fun minus(other: Cents) = Cents(value - other.value)
operator fun times(factor: Int) = Cents(value * factor)
operator fun compareTo(other: Cents) = value.compareTo(other.value)
fun toYuan() = value / 100.0
companion object { val ZERO = Cents(0) }
}
@JvmInline
value class Satoshi(val value: Long) {
operator fun plus(other: Satoshi) = Satoshi(value + other.value)
fun toBtc() = value / 100_000_000.0
}
@JvmInline
value class Currency(val code: String) {
init {
require(code.length == 3 && code.all { it.isUpperCase() }) { "Invalid currency" }
}
}
class PaymentService {
fun transfer(amount: Cents, currency: Currency, from: String, to: String): Boolean {
require(amount > Cents.ZERO) { "金额必须大于 0" }
println("Transfer ${amount.toYuan()} ${currency.code} from $from to $to")
return true
}
fun convertToBtc(usdAmount: Cents, rate: Double): Satoshi {
val btcValue = usdAmount.toYuan() / rate
return Satoshi((btcValue * 100_000_000).toLong())
}
}
fun main() {
val service = PaymentService()
val usd = Currency("USD")
val amount = Cents(1099) // $10.99
service.transfer(amount, usd, "alice", "bob")
val btcAmount = service.convertToBtc(amount, 50000.0)
println("BTC: ${btcAmount.toBtc()}")
}
效果:上线后未再发生金额单位混淆导致的资金事故。代码相比 Java 包装类版本内存占用降低 60%。
案例 2:某 SaaS 平台多租户 ID 隔离
业务场景:SaaS 平台需要严格隔离不同租户的数据,所有数据库查询都必须带租户 ID。
实现:
@JvmInline
value class TenantId(val value: String) {
init {
require(value.startsWith("tnt_")) { "Invalid tenant ID" }
}
}
@JvmInline
value class ResourceId(val value: String)
interface TenantScoped {
val tenantId: TenantId
}
data class Document(
override val tenantId: TenantId,
val id: ResourceId,
val content: String
) : TenantScoped
class DocumentRepository {
fun find(tenant: TenantId, id: ResourceId): Document? {
// 编译期保证:必须传 TenantId,不能用 ResourceId 替代
return Document(tenant, id, "content")
}
fun listByTenant(tenant: TenantId): List<Document> {
return listOf()
}
}
fun main() {
val repo = DocumentRepository()
val tenant = TenantId("tnt_001")
val doc = ResourceId("doc_001")
val document = repo.find(tenant, doc)
println("Found: $document")
// repo.find(doc, tenant) // 编译错误,避免租户 ID 与资源 ID 混淆
}
效果:彻底消除了租户数据越权访问的 bug,相比原 String ID 方案,未增加任何运行时开销。
案例 3:某 IoT 平台传感器数据
业务场景:IoT 平台接收来自不同传感器的数据,单位多样(温度、湿度、气压等),需要严格区分。
实现:
@JvmInline
value class Celsius(val value: Double) {
fun toFahrenheit() = Fahrenheit(value * 9 / 5 + 32)
operator fun plus(other: Celsius) = Celsius(value + other.value)
}
@JvmInline
value class Fahrenheit(val value: Double) {
fun toCelsius() = Celsius((value - 32) * 5 / 9)
}
@JvmInline
value class Humidity(val percent: Double) {
init {
require(percent in 0.0..100.0) { "湿度必须在 0-100 之间" }
}
}
@JvmInline
value class Hectopascal(val value: Double)
sealed class SensorReading {
abstract val sensorId: String
abstract val timestamp: Long
data class Temperature(
override val sensorId: String,
override val timestamp: Long,
val value: Celsius
) : SensorReading()
data class HumidityReading(
override val sensorId: String,
override val timestamp: Long,
val value: Humidity
) : SensorReading()
data class PressureReading(
override val sensorId: String,
override val timestamp: Long,
val value: Hectopascal
) : SensorReading()
}
class SensorProcessor {
fun process(readings: List<SensorReading>): Map<String, Double> {
val stats = mutableMapOf<String, Double>()
readings.forEach { reading ->
when (reading) {
is SensorReading.Temperature -> {
val key = "temp_${reading.sensorId}"
stats[key] = (stats[key] ?: 0.0) + reading.value.value
}
is SensorReading.HumidityReading -> {
val key = "humidity_${reading.sensorId}"
stats[key] = (stats[key] ?: 0.0) + reading.value.percent
}
is SensorReading.PressureReading -> {
val key = "pressure_${reading.sensorId}"
stats[key] = (stats[key] ?: 0.0) + reading.value.value
}
}
}
return stats
}
}
基础题
习题 1:以下代码是否能编译通过?说明原因。
@JvmInline
value class Container(val a: Int, val b: Int)
参考答案要点:
- 不能编译通过
- 原因:value class 只能有一个底层属性
- 修复:
// 方案 1:使用 data class
data class Container(val a: Int, val b: Int)
// 方案 2:用 String 编码
@JvmInline
value class Container(val encoded: String) {
val a: Int get() = encoded.substringBefore(",").toInt()
val b: Int get() = encoded.substringAfter(",").toInt()
}
习题 2:以下场景中,value class 是否会发生 boxing?
@JvmInline
value class UserId(val value: String)
fun findUser(id: UserId): User?
fun findUserNullable(id: UserId?): User?
val users: List<UserId> = listOf()
val user: Any = UserId("u1")
参考答案要点:
findUser(id: UserId):不发生 boxing(参数非空)findUserNullable(id: UserId?):发生 boxing(可空类型)List<UserId>:发生 boxing(泛型)Any:发生 boxing(装箱到 Object)
进阶题
习题 3:实现一个 Meter value class,支持与 Kilometer 互转,并实现 Comparable<Meter> 接口。
参考答案要点:
@JvmInline
value class Meter(val value: Double) : Comparable<Meter> {
override fun compareTo(other: Meter): Int = value.compareTo(other.value)
operator fun plus(other: Meter) = Meter(value + other.value)
operator fun minus(other: Meter) = Meter(value - other.value)
fun toKilometer() = Kilometer(value / 1000)
}
@JvmInline
value class Kilometer(val value: Double) {
fun toMeter() = Meter(value * 1000)
}
习题 4:分析以下代码的内存占用差异。
// 方案 A
val ids1: List<String> = (1..1000000).map { "id_$it" }
// 方案 B
@JvmInline
value class Id(val value: String)
val ids2: List<Id> = (1..1000000).map { Id("id_$it") }
参考答案要点:
- 方案 A:100 万个 String 引用,约 8MB(仅引用,不含 String 对象本身)
- 方案 B:100 万个 Id 包装对象,每个 24 字节,约 24MB
- 原因:
List<Id>中每个 Id 都需要 boxing - 结论:在集合场景下,value class 反而增加内存
挑战题
习题 5:设计一个跨平台(JVM + JS + Native)的 Money 类,要求:
- 用 value class 表示金额(以分为单位)
- 支持加减乘除
- 支持 JSON 序列化
- 跨平台兼容
参考答案要点:
// commonMain
expect annotation class JvmInline()
@JvmInline
value class Money(val cents: Long) : Comparable<Money> {
override fun compareTo(other: Money): Int = cents.compareTo(other.cents)
operator fun plus(other: Money) = Money(cents + other.cents)
operator fun minus(other: Money) = Money(cents - other.cents)
operator fun times(factor: Int) = Money(cents * factor)
operator fun div(divisor: Int) = Money(cents / divisor)
val dollars: Double get() = cents / 100.0
}
// 各平台实现 expect
// jvmMain: actual annotation class JvmInline() { ... }
习题 6:分析 value class 在 Kotlin/Native 上的实现差异。
参考答案要点:
- Kotlin/Native 不需要
@JvmInline注解 - Native 平台 value class 通过编译器直接内联实现
- Native 平台对 value class 的 boxing 时机与 JVM 略有不同(取决于运行时表示)
- 跨平台代码中应使用
expect/actual处理平台差异
官方文档
- Kotlin Inline Classes:https://kotlinlang.org/docs/inline-classes.html
- 官方对 value class 的完整说明
- Kotlin Release Notes 1.5:https://kotlinlang.org/docs/whatsnew15.html
- value class 命名变更的官方解释
- KEEP-267:https://github.com/Kotlin/KEEP/blob/master/proposals/inline-classes.md
- value class 的设计提案文档
经典教材
- 《Kotlin in Action》(Dmitry Jemerov、Svetlana Isakova 著):扩展函数与内联机制
- 《Programming in Scala》(Martin Odersky 著):Scala value class 设计哲学
- 《Real-World Haskell》(Bryan O’Sullivan 著):newtype 模式的经典介绍
前沿论文
- KEEP-267: Inline Classes(Roman Elizarov, 2018):value class 的设计动机
- Compiling with Continuations(Andrew Appel, 1992):内联优化的理论基础
- The Newtype Pattern in Haskell(Mark Jones, 1995):newtype 的早期形式化
开源项目
- kotlinx.serialization:https://github.com/Kotlin/kotlinx.serialization
- value class 的序列化支持
- Exposed:https://github.com/JetBrains/Exposed
- SQL 框架对 value class 的支持
- Ktor:https://github.com/ktorio/ktor
- 在 HTTP API 中使用 value class 作为类型安全 ID
定义内联类
基本写法:定义 value class
@JvmInline value class <类名>(val <属性>: <类型>)
// 定义内联值类避免装箱开销
@JvmInline
value class UserId(val value: Long)
基本写法:字符串包装类
@JvmInline value class <类名>(val <属性>: String)
// 包装字符串为强类型
@JvmInline
value class Email(val value: String)
使用内联类
基本写法:创建实例
<类名>(<值>)
// 创建内联类实例
val id = UserId(42L)
基本写法:访问底层值
<实例>.<属性>
// 取出底层值
val raw: Long = id.value
基本写法:作为参数
fun <方法>(<参数>: <内联类>) {}
// 函数签名使用强类型
fun find(id: UserId) = repo.findById(id.value)
内联类方法
基本写法:定义方法
@JvmInline value class <类>(val v: <类型>) { fun <方法>() {} }
// 内联类可定义方法
@JvmInline
value class Meter(val value: Double) {
fun toKm() = Kilometer(value / 1000)
}
基本写法:扩展函数
fun <内联类>.<方法>() {}
// 为内联类添加扩展
fun Meter.toCm() = value * 100
内联类与接口
基本写法:实现接口
@JvmInline value class <类>(val v: <类型>) : <接口> {}
// 内联类实现接口
interface Printable { fun print() }
@JvmInline
value class Name(val value: String) : Printable {
override fun print() = println(value)
}
泛型与内联类
基本写法:泛型内联类
@JvmInline value class <类><<T>>(val v: <T>)
// 泛型内联类
@JvmInline
value class Box<T>(val value: T)
nullable 内联类
基本写法:可空类型字段
@JvmInline value class <类>(val v: <类型>?)
// 内部可空时装箱为包装类
@JvmInline
value class Phone(val value: String?)
underlying 字段类型
基本写法:数字底层类型
@JvmInline value class <类>(val v: <数字类型>)
// 底层为 Int 编译期不装箱
@JvmInline
value class Age(val value: Int)
与 Java 互操作
基本写法:Java 调用需传底层类型
<Java 方法>(<底层值>);
// Java 中调用需传 Long 而非 UserId
// Kotlin 端添加 @JvmName 避免签名冲突
@JvmInline
value class UserId(val value: Long) {
@JvmName("of") companion object { fun of(v: Long) = UserId(v) }
}
value class 与 data class 区别
基本写法:何时用 value class
@JvmInline value class <类>(val v: <单字段>)
// 单字段且无需 equals 时使用 value class
@JvmInline value class Token(val value: String)
基本写法:何时用 data class
data class <类>(val <字段1>, val <字段2>)
// 多字段或需 equals 时使用 data class
data class Point(val x: Int, val y: Int)
内联类集合
基本写法:内联类作为 Map 键
Map<<内联类>, <值>>
// 内联类作为 Map 键避免装箱
val map: Map<UserId, String> = mapOf(UserId(1) to "Alice")
Sealed 结合内联类
基本写法:内联类配合密封类
sealed class <基> | @JvmInline value class <子>: <基>
// 内联类作为密封类子类
sealed interface Id
@JvmInline value class LongId(val v: Long) : Id
@JvmInline value class Uuid(val v: String) : Id
验证构造
基本写法:init 校验
@JvmInline value class <类>(val v: <类型>) { init { require(<条件>) } }
// 构造时校验合法性
@JvmInline
value class NonBlank(val value: String) {
init { require(value.isNotBlank()) { "blank" } }
}
性能优化场景
基本写法:ID 类型避免装箱
@JvmInline value class <Id>(val v: <Long>)
// 大量 ID 时用内联类避免 Long 装箱
@JvmInline value class OrderId(val v: Long)
val ids = listOf(OrderId(1), OrderId(2)) // 数组无装箱
toString 与 equals
基本写法:自定义 toString
override fun toString() = "<格式>"
// 自定义字符串表示
@JvmInline
value class UserId(val value: Long) {
override fun toString() = "User($value)"
}
内联类限制
基本写法:只能有单构造属性
@JvmInline value class <类>(val <属性>: <类型>)
// 仅支持单个 val 属性作为底层值
@JvmInline value class Score(val value: Int)
基本写法:内部不能有可变状态
@JvmInline value class <类>(val v: <类型>) { var <变量> } // 禁止
// 内联类不能有可变 var 属性
@JvmInline value class Score(val value: Int)