Kotlin的值类(Value Class)是一种强大的类型安全工具,允许开发者创建语义明确的类型,并保持运行时零成本。
假设系统中存在用户的概念,用户拥有名字和电子邮箱地址。用户名和电子邮箱地址都是长度不超过120个字符的字符串。用户名不能是空白,不能是"null",也不能包含"@"。电子邮箱地址必须包含"@"。根据这些要求,我们可以得到一个简单的模型。
代码1 简单的User模型
data class User(val name: String, val email: String)
这个模型没有对值进行校验,客户端代码可能直接调用 user.name = "null"
,产生一条不满足业务约束的数据。为了避免这种情况,我们可以为用户名、电子邮箱分别建立模型。
代码2 复杂的User模型
data class User(val name: UserName, val email: Email)class UserName(val value: String) {init {require(!value.contains("@") && ... ) { "Invalid userName" }} }class Email(val value: String) {init {require(value.contains("@")) { "Invalid email" }} }
复杂模型可以保证业务逻辑不出错,但多了一个包装对象,产生运行时性能损耗。仔细观察UserName和Email两个类,都是把一个String对象和一些专属操作绑定起来,构成一个新类型。新类型可以表达语义,操作可以校验值。这两样都是我们需要的。有没有办法既能做到这两点,又不会产生额外的包装对象呢?答案就是值类。
代码3 值类:“基础”类型零成本抽象
@JvmInline value class UserName(val value: String) {init {require(!value.contains("@") && ... ) { "Invalid userName" }}}
从语法上看,值类版本的UserName只比普通版本多了 @JvmInline
注释,并且将 class
换成了 value class
,其他方面并无差别。但在运行时,值类不会产生额外性能损耗,可以做到零成本抽象。
值类能做到运行时零成本的方法和C++模板或TypeScript类似,编译时在字节码级别进行内联。比如下面的值类
@JvmInline value class Meter(val value: Double)fun calculate(m: Meter) = m.value * 2
编译后的字节码等价于
public static double calculate(double m) {return m * 2; }
因此值类可以做到:
- 没有额外对象分配
- 没有虚方法表
- 没有对象头开销
- 方法调用转为静态分派
当然值类的使用也存在一些限制,包括:
- 不能声明多个属性
- 不能继承其他类(可以实现接口)
- 不能在反射场景中使用
- 需要特殊处理泛型场景
JVM泛型需要对象,因此在泛型中使用值类会引发装箱。
// 触发装箱 val list = listOf(UserId("123")) // 方案1:使用原始类型数组避免装箱(推荐) val array = arrayOf(UserId("123"))// 方案2:通过inline class+类型投影减少装箱 val list = listOf<UserId>(UserId("123"))
值类的使用场景有:
- 需要区分语义相似的原始类型时 (名字, 邮件等)
- 需要为简单值添加领域行为时
- 高频调用的基础类型包装
- 要求极致性能的数值计算场景
- 大型项目中的领域模型定义
需要避免值类的场景有:
- 需要包装多个字段的复杂对象
- 需要复杂继承关系的类型
- 深度依赖反射的操作
- 与某些Java框架深度集成的场景
值类的核心优势在于:
- 编译时类型安全
- 领域语义明确
- 零成本抽象
- 减少模型转换样板
- 增强代码可读性和可维护性
特性 | 值类 | 数据类(Data Class) |
---|---|---|
内存开销 | 零(运行时内联) | 每个对象额外16-24字节对象头 |
适用场景 | 单值包装 | 多属性数据容器(如DTO) |
自动生成方法 | 仅基于包装值的方法 | equals()/hashCode()/copy()等 |
泛型处理 | 可能触发装箱 | 直接支持 |
特性 | 值类 | 装箱(以Integer为例) |
---|---|---|
设计目标 | 类型安全的语义增强 | 原始类型与对象类型的转换桥梁 |
内存开销 | 0 (编译时内联) | Integer: 16+字节对象头 |
类型系统 | 创建真正的新类型 | int和Integer是相同值的不同表示 |
空值安全 | 默认非空 (显式声明可空) | int不能null, Integer可为null |
集合性能 | 等同于原始类型集合 | 对象指针集合 (内存碎片化) |
使用场景 | 领域建模中的语义化类型 | 泛型兼容和对象类型需求 |
维度 | 值类 | DDD值对象 (Value Object) |
---|---|---|
范畴 | 编程语言特性 (Kotlin特有) | 领域驱动设计(DDD)概念 |
核心目的 | 零开销的类型安全包装 | 表示没有唯一标识的领域概念 |
实现方式 | @JvmInline value class | 不可变类(通常用 data class) |
身份标识 | 无明确要求 | 无唯一标识 (靠属性值区分) |
相等性 | 基于包装值 (可自定义) | 基于所有属性值 |
可变性 | 默认可变 (但通常设计为不可变) | 严格不可变 |
典型应用 | ID包装、单位封装、类型别名 | 金额、地址、日期范围、坐标点 |
值对象(Value Object)是领域驱动设计中不可变的概念片段,值类是Kotlin零开销的类型安全包装特性。二者主要是名称相似。如果当值对象只需封装单个值时,值类是最佳实现方式。
维度 | 值类 | 扩展方法 |
---|---|---|
本质 | 创建新类型 | 扩展现有类型 |
类型系统 | 编译时引入新类型 (运行时内联) | 不引入新类型 |
作用范围 | 全局性的类型安全增强 | 局部性的功能增强 |
主要目的 | 解决类型安全问题 | 解决功能扩展问题 |
使用方式 | 创建新类型实例 | 在现有类型实例上调用 |
性能影响 | 零运行时开销 | 极低开销(静态方法调用) |
领域建模 | 核心领域概念建模 | 辅助功能实现 |