其实对于初学者来说,序列化其实是不好理解的,然后我也是属于其中的一员,感觉始终不是很理解这些东西,然后通过询问ai,我有了自己的理解,如果有不对欢迎指出谢谢。
我对序列化的理解就一句话:它的作用就是把对象转成二进制,用来传输和存储。
它就是为了让我们传输和存储java对象的,所以不要觉得它有多高大上。然后我们自己接触的很少,因为一般都是框架帮我们做了,虽然我们没做,这就跟反射一样,但是它其实一直在发生。
比如,我的前端发来一个 JSON 请求,我 Controller 方法里那个被 @RequestBody 修饰的 Java 对象,是怎么"变"出来的?这是反序列化 。Spring Boot 默认用了 Jackson 框架,替我把 JSON 字符串转成了 Java 对象。我返回一个对象给前端,那个对象又是怎么变成 JSON 字符串通过网络传回去的?这是序列化。同样是 Jackson,默默地在背后替我做了。
还有我用 RabbitMQ 发送消息,消息体里放着 Java 对象。RabbitMQ 本身不认 Java 对象,它只认二进制数据。所以,我的对象在被发送出去之前,必须被序列化 成字节流;消费者收到字节流后,也必须反序列化回 Java 对象。这一切,也是框架在背后默默完成的。
所以,"为了传输和存储" 这个理解依然是核心。但它不是空洞的理论,而是在我项目里时刻都在发生的事实。只是因为这些脏都被框架(比如Jackson)、工具(比如RabbitMQ)默默地做了,才感觉它好像离我们很远。
在理解上面的基础上在理解深层次的东西。
如果我的对象结构变了怎么办?
比如,我的第一个版本里,User 类只有 name 和 age。业务升级,我加了一个 email 字段。新的对象当然没问题,但问题是,我已经存进 Redis 的那些旧数据怎么办?它们只有 name 和 age,现在要反序列化成带有 email 的新 User 对象,程序会不会崩?
如果是Jackson我加了一个 email 字段。当我用新的 User 类去反序列化这条旧 JSON 数据时,Jackson 会淡定地发现,JSON 里没有 email 这个字段。它不会崩溃,而是会直接给新字段设一个默认值,比如 null。反过来,如果 JSON 里多了一个 phone 字段,但我的 User 类里没有,Jackson 也只会忽略掉它,不会报错。你少了,我给你补个默认值;你多了,我就当没看见。
如果是java序列化的话这就是 serialVersionUID 要解决的问题。如果我显式定义了这个 ID,我加一个字段,ID 不变,JVM 就认为它们还是同一个类。反序列化时,它会把新增的 email 字段设为 null,然后继续正常工作。这样,新老版本的数据就兼容了,旧数据不会变成"僵尸"。如果没有显示定义,会根据数据结构来生成这个uid,所以一旦结构变化,那么旧数据无法反序列化因为会认为他们不是一个东西。
有些东西我不想被序列化怎么办?
这是真实的安全需求。我的 User 对象里肯定有 password 字段。如果我直接把 User 对象序列化成 JSON 返回给前端,密码就泄露了。
解决这个问题也很简单。如果是 Java 原生序列化,就用 transient 关键字标记这个字段。如果是 JSON 序列化(就像我项目里用的 Jackson),就用 @JsonIgnore 注解。被打上标记的字段,在序列化时会被直接跳过,反序列化出来就是默认值(null、0、false)。
序列化的多种方式
java 原生序列化、JSON、Protobuf...这些都是在不同场景下的不同选择。Java 原生序列化兼容性最好,但生成的数据太大,也不跨语言;JSON 跨语言、可读性好,是现在用得最多的,我项目里也是用这个;Protobuf 性能高、数据最小,但可读性差。
所以,我后来对序列化的理解更新为 :序列化不仅是一种数据转换的技术,更是一套完整的"对象进化兼容方案 "。它通过 serialVersionUID 解决了版本兼容问题,通过 transient / @JsonIgnore 解决了隐私保护问题,并通过多种实现框架提供了灵活的控制权。