不同的序列化框架对重复引用的支持程度存在显著差异。例如,Java 原生序列化、Jackson、Gson 等框架都提供了处理重复引用的机制,能够在序列化和反序列化过程中维护对象间的引用关系。
Kotlin Serialization 不支持重复引用处理,默认行为是将每个引用都完整序列化,下面我们将结合 Kotlin 来探讨重复引用问题。
1. 重复引用带来的问题
重复引用主要会引入三个问题:
- 存储开销:重复引用序列化导致 JSON 体积增大
- 内存消耗:反序列化时创建多个相同内容的对象实例
- 功能完整性:如果对象为不可变数据类,多实例不会破坏业务逻辑的正确性
结论 :当存储和内存的额外开销在可接受范围内,且重复实例是不可变数据类时,重复引用问题可以被忽略。如果不满足这个条件,重复引用并不是你实际要解决的问题。🤪🤪🤪🤪🤪
Kotlin 中的不可变数据类
强烈建议使用 data class
遵循以下原则构建不可变数据类:
- 所有属性都声明在主构造函数中
- 所有属性都使用
val
关键字修饰 - 属性类型本身也应该是不可变的(如基本类型、不可变集合等)
- 避免在类体中定义可变的成员变量
2. 序列化支持重复引用是个伪命题?
当业务强依赖重复引用问题时,这通常是架构设计缺陷的外在表现,而非序列化框架本身的局限。
数据模型优化建议
- 用 ID 代替对象:不直接引用对象,改用 ID 标识来表示关系
- 严格分层设计:序列化数据模型、UI 模型、业务模型应该严格区分,避免混用
- 保持简洁性:用于序列化的数据模型在设计上应该保持简洁,只包含必要的数据字段
3. 循环引用:超越序列化的架构问题
循环引用的危害远不止序列化层面,它是一个影响整个系统架构质量的根本性问题。
循环引用的系统性危害
- 修改风险:对象关系复杂,理解成本高,修改可能引发连锁反应
- 深拷贝操作复杂:对象复制变得异常困难,甚至可能导致无限递归
- 比较运算复杂:对象相等性判断需要额外的循环检测机制
4. Kotlin Serialization 重复引用解决方案(学习示例)
重要声明:以下技术方案仅用于序列化实践,展示技术可能性。在实际项目中,强烈建议通过优化架构设计来避免重复引用问题,而非依赖这些复杂的技术手段。
问题背景
在 JSON 反序列化过程中,当遇到具有相同标识符的对象时,确保它们在内存中指向同一个实例,从而实现真正的引用共享。
实现策略
- 构建专用 Json 配置:为需要处理重复引用的类型注册上下文序列化器,避免缓存污染,应该尽量收紧 Json 实例的生命周期
- 实例缓存机制:序列化器维护对象缓存,根据唯一标识符管理实例,避免重复对象产生
- 注解标记 :使用
@Contextual
注解标记需要特殊处理的属性 - 智能实例复用:反序列化时优先从缓存中获取已存在的对象实例
通过这种方案,可以深入理解 Kotlin 序列化器动态配置技巧。
示例代码
反序列化目标类型
kotlin
@Serializable
data class User(val id: Long, val name: String)
@Serializable
data class Order(
val id: String,
@Contextual val buyer: User, // 添加上下文序列化器注解
@Contextual val seller: User,
@Contextual val operator: User,
)
定义上下文序列化器
kotlin
class UserContextualSerializer : KSerializer<User> {
private val userCache = mutableMapOf<Long, User>()
override val descriptor = buildClassSerialDescriptor("User") {
element<Long>("id")
element<String>("name")
}
override fun deserialize(decoder: Decoder): User {
return decoder.decodeStructure(descriptor) {
var id = 0L
var name = ""
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> id = decodeLongElement(descriptor, 0)
1 -> name = decodeStringElement(descriptor, 1)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
// 优先从缓存中获取相同ID的对象
userCache.getOrPut(id) { User(id, name) }
}
}
override fun serialize(encoder: Encoder, value: User) {
encoder.encodeStructure(descriptor) {
encodeLongElement(descriptor, 0, value.id)
encodeStringElement(descriptor, 1, value.name)
}
}
}
kotlin
class RepeatUserSerializer {
private val format = Json {
serializersModule = SerializersModule {
contextual(User::class, UserContextualSerializer())
}
}
// 反序列化入口方法
fun decodeFromString(jsonStr: String): Order {
return format.decodeFromString<Order>(jsonStr).also(::printAnalysis)
}
// 引用分析输出
private fun printAnalysis(order: Order) {
println("=== 对象引用分析 ===")
println("buyer hashCode: ${order.buyer.hashCode()}")
println("seller hashCode: ${order.seller.hashCode()}")
println("operator hashCode: ${order.operator.hashCode()}")
println("buyer === seller: ${order.buyer === order.seller}")
println("buyer === operator: ${order.buyer === order.operator}")
println("seller === operator: ${order.seller === order.operator}")
}
}
测试输入
json
{
"id": "order1",
"buyer": {
"id": 10001,
"name": "Alice"
},
"seller": {
"id": 10001,
"name": "Alice"
},
"operator": {
"id": 20001,
"name": "Bob"
}
}
运行结果分析
ini
=== 对象引用分析 ===
buyer hashCode: 63660399
seller hashCode: 63660399
operator hashCode: 686996
buyer === seller: true
buyer === operator: false
seller === operator: false
buyer
和seller
具有相同的 hashCode 且===
比较为 true,证明它们是同一个对象实例operator
具有不同的 hashCode 且与其他对象的===
比较均为 false,说明它是独立的对象实例- 这验证了上下文序列化器成功实现了基于 ID 的对象实例复用机制
总结与思考
-
重复引用不是技术问题,而是设计问题:当我们需要在序列化层面处理复杂的引用关系时,往往说明底层的架构设计存在缺陷。
-
不可变性是最佳防线:严格遵循不可变数据类的设计原则,可以从根本上避免大部分重复引用相关的问题。
-
简单性胜过复杂性:优秀的架构设计应该让序列化变得简单直观,而不是需要复杂的技术手段来解决问题。