









一、深拷贝 vs 浅拷贝
1. 核心原理与区别:浅拷贝只是复制引用地址,而深拷贝新开辟了内存空间
内存表现举例:
假设有一个 Person 类,包含 name(String)和 address(Address
对象):
2. 浅拷贝的实现方式
方式一:手动实现 Cloneable 接口并重写 clone() 方法
super.clone() 底层由 JVM 实现,会创建一个与原对象类型相同的新对象,并逐字段复制内存值。但这对引用类型只复制引用地址,所以是浅拷贝。
方式二:BeanUtils.copyProperties(Spring自带)
底层通过反射获取源对象的所有属性,然后逐个调用 setter 方法赋值。对于引用类型,同样只复制引用地址。
3. 深拷贝的实现方式
方式一:手动递归 new(最直观但最繁琐)
优点:完全可控,可以自定义拷贝逻辑。缺点:每增加一个引用类型字段都要修改拷贝方法,维护成本高,容易遗漏。
方式二:通过序列化实现深拷贝(最简洁)
原理:序列化将对象写入字节流时,会递归序列化所有引用类型的对象;反序列化时,JVM 会在堆内存中重新分配所有对象的内存空间,因此天然实现了深拷贝。
方式三:第三方库(如 Kryo)
Kryo 等第三方库通过更高效的方式实现深拷贝,性能远高于 Java 原生序列化方式。
方式四:Hutool 的 BeanUtil.copyProperties
底层会自动创建新的对象
4. 使用场景与避坑指南
使用场景:
常见坑:
二、序列化体系:Java 原生序列化 vs JSON 序列化
1. 什么是序列化?
序列化(Serialization)是将内存中的对象状态转换为可存储或可传输的字节流的过程。反序列化则是将字节流还原为内存中的对象。核心目的有两个:持久化存储和网络传输。
2. Java 原生序列化(ObjectOutputStream)
底层原理:
Java 原生序列化是 JVM 级别的底层机制。序列化时,JVM
通过反射获取类的所有字段(getDeclaredFields()),逐一检查字段的修饰符:
反序列化时,JVM 会直接在堆内存中"凭空"分配一块空间,创建一个全新的对象实例,完全绕过构造函数(不会调用任何 Constructor),然后将字节流中的二进制数据直接"填"进这块内存里。
使用场景:
3. JSON 序列化(Jackson / Fastjson / Gson)
底层原理:
JSON 序列化是纯 Java 应用层的代码,底层大量使用反射机制。
使用场景:
4. 两者核心差异对比表
表格
|
对比维度 |
Java 原生序列化 |
JSON 序列化(Jackson / Fastjson) |
|
数据读取方式 |
反射直接读取内存中的字段(含 private) |
反射调用 getter 方法 |
|
数据写入方式 |
反射直接修改内存中的字段(含 private) |
反射调用 setter 方法 |
|
对象创建方式 |
绕过构造函数,直接在堆内存分配空间 |
反射调用无参构造函数 new |
|
依赖的接口 |
必须实现 Serializable |
无要求,普通 POJO 即可 |
|
输出格式 |
二进制字节流 |
纯文本(JSON 字符串) |
|
跨语言能力 |
仅限 Java 生态 |
跨语言通用 |
|
transient 影响 |
会被跳过,不序列化 |
取决于框架(Jackson/Gson 跳过,Fastjson 不跳过) |
三、transient 关键字与自定义序列化
1. transient 的核心作用
transient 是 Java 原生序列化中的一个关键字。它的作用是告诉 JVM:在序列化时跳过这个字段,不要将它写入字节流。反序列化时,被 transient 修饰的字段会被赋予类型的默认值(引用类型为 null,int 为 0,boolean 为 false 等)。
2. 三大典型使用场景
3. 框架差异陷阱:transient 对 JSON 框架的不同影响
实战建议:让 transient 回归它原生序列化的本职工作。在 JSON 序列化时,使用各框架自己的注解来控制字段的显示与隐藏(如 Jackson 的 @JsonIgnore、Fastjson 的 @JSONField(serialize = false))。
4. 进阶:自定义序列化逻辑(readObject / writeObject)
Java 原生序列化提供了自定义序列化机制。如果你的类中定义了以下私有方法,JVM 会在序列化/反序列化时自动调用:
四、安全与工程实践
1. serialVersionUID 的作用与最佳实践
serialVersionUID 是 Serializable 接口的版本号。JVM 在反序列化时会检查类版本是否匹配。如果版本号不一致,会抛出 InvalidClassException。
最佳实践:始终显式定义 serialVersionUID(如 private static final long serialVersionUID = 1L;)。这样可以保证即使类结构发生变化,旧数据仍然可以反序列化(虽然新增的字段会是默认值)。
2. 反序列化安全漏洞(RCE 漏洞原理与防御)
漏洞原理:Java 原生序列化在反序列化时,会直接执行字节流中对象图里所有类的 readObject 方法。如果攻击者能控制输入的字节流,就可以构造恶意的序列化数据,在反序列化时触发任意代码执行(RCE)。
防御手段:
3. 实战常见踩坑总结
|
知识点 |
核心要点 |
|
浅拷贝 |
复制基本类型值,引用类型只复制引用地址,共享同一个对象 |
|
深拷贝 |
递归复制所有引用类型对象,原对象和拷贝对象完全独立 |
|
深拷贝最佳实现 |
通过序列化实现,代码最简洁;手动递归 new 性能高但维护成本高 |
|
Java 原生序列化 |
JVM 级别直接操作内存,通过反射读取 private 字段,绕过构造函数 |
|
JSON 序列化 |
应用层反射调用 getter/setter,输出纯文本,跨语言通用 |
|
两者核心区别 |
原生序列化操作内存(含 private),JSON 序列化通过 getter(公开接口) |
|
transient 作用 |
告诉 JVM 序列化时跳过该字段,反序列化时恢复为默认值(就是null) |
|
transient + JSON |
Jackson/Gson 会跳过,Fastjson 不会跳过(只认 getter) |
|
transient 最佳实践 |
用各框架自己的注解控制 JSON 序列化,transient 仅用于原生序列化 |
|
readObject/writeObject |
自定义序列化逻辑,可实现加密、懒加载、动态重建资源 |
|
serialVersionUID |
版本号,反序列化时校验类版本,必须显式定义 |
|
反序列化漏洞 |
恶意字节流在反序列化时执行任意代码,防御手段是白名单校验 |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。