Skip to content

Latest commit

 

History

History
245 lines (192 loc) · 9.64 KB

File metadata and controls

245 lines (192 loc) · 9.64 KB
title date tags categories
Java中的序列化
2019-04-05 15:38:44 -0700
Java
序列化

💠

💠 2025-12-14 19:15:49


Java中的序列化

码农翻身:序列化: 一个老家伙的咸鱼翻身 对象转化为二进制流

  • 序列化: 将数据结构或对象转换成二进制串的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程
    • 反序列化生成对象时不会调用对应类的构造器

Note:序列化语言无关
jvm-serializers多种框架的基准测试

默认序列化, 显式序列化, 拷贝不变(trivially copyable) 和FlatBuffers思想类似

Serializable

简单的说serialVersionUID就是类的版本控制, 标明类序列化时的版本, 版本一致表明这两个类定义一致
在进行反序列化时, JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

  • serialVersionUID有两种显示的生成方式:
    • 一种是固定常量值,例如1L
    • 一种是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段

当你一个类实现了Serializable接口,如果没有定义serialVersionUID,可通过IDE进行提醒显示定义。如果要使用JDK的序列方式, 一定要显式定义, 且后续不能改, 除非业务上确认不兼容.

子接口 Externalizable, 实现writeExternal()和readExternal()⽅法可以指定序列化哪些属性

JDK序列化和反序列化

    TargetObject targetObject = new TargetObject("name");
    ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
    ObjectOutputStream output = new ObjectOutputStream(byteOutput);
    output.writeObject(targetObject);

    ByteArrayInputStream byteInput = new ByteArrayInputStream(byteOutput.toByteArray());
    ObjectInputStream input = new ObjectInputStream(byteInput);
    TargetObject result = (TargetObject) input.readObject();
    assertThat(result.getName(), equalTo("name"));
  • 在做有多态结构的对象深拷贝时,使用JDK序列化方式能简单且快速实现。但如果使用JSON序列化方式来实现,需要解决节点类型信息丢失的问题
    • 例如一个多叉树上的节点是一个接口的多类型实例。
        public interface Node {
            List<Node> getChildes();
        }
        @Data
        public class Dir implements Node {
            private List<Node> childes;
        }
        @Data
        public class File implements Node {
            private List<Node> childes;
        }

Externalizable (基本不用到)

ExternalizableSerializable 的子接口,提供了更细粒度的序列化控制。通过实现 writeExternal()readExternal() 方法,可以完全自定义序列化和反序列化的过程。

与 Serializable 的区别

特性 Serializable Externalizable
序列化方式 自动序列化所有非 transient 字段 手动控制序列化哪些字段
方法实现 无需实现方法(可选 writeObject/readObject) 必须实现 writeExternal() 和 readExternal()
性能 相对较慢(反射机制) 相对较快(直接控制)
灵活性 较低 较高
构造器调用 反序列化时不调用构造器 反序列化时会先调用无参构造器

基本使用

import java.io.*;

public class User implements Externalizable {
    private String name;
    private int age;
    private transient String password; // transient 字段不会被自动序列化
    
    // Externalizable 要求必须有无参构造器
    public User() {
        System.out.println("无参构造器被调用");
    }
    
    public User(String name, int age, String password) {
        this.name = name;
        this.age = age;
        this.password = password;
    }
    
    // 自定义序列化逻辑
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
        // 可以选择不序列化 password,或者进行加密后序列化
        // out.writeObject(password);
    }
    
    // 自定义反序列化逻辑
    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String) in.readObject();
        age = in.readInt();
        // 对应 writeExternal 中的读取顺序
        // password = (String) in.readObject();
    }
    
    // getter/setter 方法...
}

序列化和反序列化示例

// 序列化
User user = new User("张三", 25, "secret123");
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
ObjectOutputStream output = new ObjectOutputStream(byteOutput);
output.writeObject(user);
output.close();

// 反序列化
ByteArrayInputStream byteInput = new ByteArrayInputStream(byteOutput.toByteArray());
ObjectInputStream input = new ObjectInputStream(byteInput);
User deserializedUser = (User) input.readObject();
input.close();

// 注意:反序列化时会先调用无参构造器,然后调用 readExternal 方法

注意事项

  1. 必须有无参构造器Externalizable 要求类必须提供 public 无参构造器,因为反序列化时会先调用它
  2. 读写顺序一致writeExternal()readExternal() 中的字段读写顺序必须完全一致
  3. 版本兼容性:与 Serializable 一样,修改类结构时需要注意版本兼容性
  4. 性能考虑:虽然性能更好,但需要手动维护序列化逻辑,代码复杂度更高

使用场景

  • 需要精确控制序列化哪些字段
  • 需要对某些字段进行加密或特殊处理
  • 性能敏感的场景
  • 需要序列化 transient 字段(通过手动实现)

编解码框架

因为Java序列化的性能和存储开销都表现不好,而且不能跨语言, 所以一般不使用Java的序列化而是使用以下流行的库

LZ4

Github

fast-serialization

Github 10倍于JDK序列化性能而且100%兼容的编码

Snappy

Github

Kryo

Github

基准测试中得分最高的框架

JSON

Github Topic: java-json

Jackson在将Date对象序列化成时间时,默认使用的是UTC零时区,Fastjson是使用系统时区。

# 设置jackson序列化时区
spring.jackson.time-zone=Asia/Shanghai
spring.jackson.serialization.write-dates-as-timestamps=false

Protobuf

Note
Protocol Buffer Basics: Java

hi.proto 快速试用

    package lm;
    message helloworld{
        required int32 id = 1;//ID
        required string str = 2;//str
        optional int32 opt = 3;//optional field
    }
  • 由 proto 编译生成 Java 类: mkdir src && protoc --java_out=./src hi.proto

工程内使用流程简述: 通过插件将proto文件编译到指定目录(该目录设置为source并被git忽略)下的Java类, 项目编译和运行时就可以使用这些类,注意修改了协议文件就需要手动编译一次
插件: maven-protoc-plugin 或 protobuf-gradle-plugin


Marshalling

JBOSS 内部使用的编解码框架


Tips

JSON字符串反序列化时泛型丢失问题

  1. Jackson 方式 需要先配置 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    1. 第一种 objectMapper.writeValueAsBytes
    2. 第二种 objectMapper.readValue(bytes, 0, bytes.length, Object.class)