什么是序列化和反序列化?

  1. 序列化,是将对象转化为字节流的过程,即将数据转换为字节流
  2. 反序列化,是将字节流重新转换为对象的过程,即为从数据中读取并重新创建对象

为什么要序列化?

你可能会问,为什么要序列化,它的作用是什么?
其实序列化的最终目的是为了能够人让对象可以跨平台存储,并且让其代码能实现网络传输
我们在进行跨平台存储和网络传输时的方式就是IO,而IO流支持的格式便是字节数组
如果我们单方面的只是那对象转成字节数组还不行,因为我们还要将它还原回来,一个没有规则的字节数组我们是没有办法将对象转换为原来的面貌的,所以我们必须把对象转成字节数据的时候,就要指定规则(序列化),那么我们从Io流里面读取数据的时候,便又可以还原回来(反序列化)

什么情况下需要进行序列化?

上面其实已经说明了,在进行**“跨平台存储”“网络传输”**的数据,都需要进行序列化。

如何序列化?

在 java 中,如果一个对象要实现序列化,必须实现下面两个接口之一

  • Serializable 接口
  • Externalizable 接口

Serializable 接口

一个对象需要被序列化,那么它的类就要实现此接口或者它的子接口

这个对象的所有字段(属性)包括用 private 修饰的和其引用的对象,都可以被序列化和反序列化来传递和保存。不想被序列化的字段可以用 transient 修饰。

由于 Serializable 对象是完全以它存储的 二进制为基础来构造的,因此它不会调用任何构造函数,所以它不需要默认构造函数,但是用Serializable类作为父类没有实现Serializable接口时,反序列化过程会默认调用父类的默认构造函数,因此该父类必须有默认的构造函数。

小提醒:使用 transient 阻止序列化虽然简单方便,但被它修饰的属性会被完全隔离在序列化的机制之外,导致在反序列化中获取不到该字段(属性)的值,解决的方式也有,只需要在需要序列化对象的类中加入 writeObject 方法与 readObject 方法便可控制如何序列化各属性,甚至不序列化某些属性或者加密序列化某些属性。

Externalizable 接口

本质上来说还是 Serializable 接口,因为 ExternalizableSerializable子接口,需要手动实现 writeExternalreadExternal 方法,来解决序列化和反序列化操作。

因为 transient 关键字就是 通过手动设置 这两个方法来实现的自定义隔离,所以,在此接口中,无效

对于 Externalizable 对象被反序列化时,会先调用类的无参构造方法,这是与默认反序列化有区别的。如果将类的无参构造方法删除,或者是将该无参构造方法的权限修饰符改为 private、默认或者 protected 级别,会抛出 java.io.InvalidException: no valid constructor 异常,因此Externalizable 对象必须有默认构造方法,且必须为 public

对比

在使用时,你如果只想隐藏一个属性,例如用户(user)的密码(pwd),房间号的密码等等,如果使用了Externalizable,那么除了pwd之外的每一个属性,你几乎都要写在 writeExternal 里面,大大的增加了方法的复杂度,而Serializable 接口就不一样,只需要在隐藏的属性 pwd 中 加上一个 transient 就行了。

当然你可能回想,Serializable 中的 writeObjectreadObject 方法也可以实现自定义序列化,那么他们与 Externalizable 中的writeExternalreadExternal 的区别在哪?

  • 他们之间除了方法名不一样之外,其方法体式完全一样
  • 需要指出的是,当使用 Externalizable 的机制 反序列化时,程序会使用 public 的无参构造器创建实例,然后执行 readExternal 方法进行反序列化,因此实现 Externalizable 的序列化类 必须提供 public 的无参构造
  • 虽然实现Externalizable 接口能带来一定的性能提升,但由于实现Externalizable 接口会导致程序的复杂度提高,所以一般是采用实现Serializable接口方式来实现序列化。

如何使用 Serializable 进行序列化?

话不多说,上代码:
user类:

public class user implements Serializable {
    private String name;
    private String pwd;

    @Override
    public String toString() {
        return "user{" +
                "name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
    // 此处省略
 }

序列化:

public static void serializable() throws IOException {
        User user = new User();

        user.setName("sleep");
        user.setPwd("a1w3");

        ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files
                .newOutputStream(new File("info.txt").toPath()));
        objectOutputStream.writeObject(user);
        objectOutputStream.close();

        System.out.println("序列化成功!已经成功生成 info.txt 文件了");
        System.out.println("================================================");
    }

反序列化:

public static void deserializable() throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(Files
                .newInputStream(new File("info.txt").toPath()));
        User user = (User) objectInputStream.readObject();
        objectInputStream.close();

        System.out.println("反序列化结果为:");
        System.out.println(user);
    }

运行结果:
运行成功结果
实际上的 info 文件:
info.txt

Serializable 有什么作用?

虽然我们说要实现序列化就要加上 Serializable 或者 Externalizable,但我们点进 Serializable 接口时发现,它只是一个空接口:
Serializable接口
那么如果我们将 implements Serializable 去除,会怎么样?
去除后的结果
这里直接报错 NotSerializableException,当我们点击at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184),查看原因是发现:
报错源码处
这一大段的意思是,如果一个对象,它不是字符串、数组或者枚举时,也没有实现 Serializable 接口,则在序列化时,直接报错,抛出 NotSerializableException 异常。

这个时候我们可以确信,Serializable 接口的作用就是一个标志物,一个标记,它的作用仅仅就是告知程序,这个对象或者这个类可以被序列化,实际上的 序列化动作 并不是它完成的。

serialVersionUID号有什么用?

你肯定在某些序列化类中,看到过类似下面的东西:

private static final long serialVersionUID = -4232658600822850859L;

那么这个玩意是干嘛的呢?
添加在 user 类之后,继续我们的代码实践,还是 user 类,我们执行完上面的方法之后,会保留一个info.txt,假设,我们的业务中,需要为 user 类新添字段 id,添加完之后,再执行一遍反序列化,会怎么样呢?

public class User implements Serializable {
    private String name;
    private String pwd;
    // 后续 添加
    private Integer id;


    @Override
    public String toString() {
        return "user{" +
                "name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
    // 此处省略
}

执行反序列化后,报错:
添加字段后报错
这里直接告诉了我们说:前后的 serialVersionUID 不一致,这个时候我们知道了最少两点:

  1. serialVsersionUID 是序列化前后的唯一标识
  2. 如果在 序列化的类中未声明 serialVersionUID,系统会默认生成一个

这个时候我们就可以回答这个问题了,serialVsersionUID的作用就是一个序列化前后的一个对标,打个比方,一个人去朋友家,需要知道他家的地址才能找到,并且到达。
所以,在反序列化时,JVM 会将字节流中序列号ID 和 被序列化类中的ID进行一个比较,一致时,才能进行反序列化,否则,将抛出异常来终止序列化,值得注意的是,如果你未规定 serialVsersionUID,一旦像上面一样更改后,随之的 serialVsersionUID 也会改变!

所以一般的良好代码习惯是写上 serialVsersionUID。

两种特殊情况

  1. 凡是被 static 修饰过后的字段是不会被序列化的
  2. 凡是被 transient 修饰过后的字段也不会被序列化

第一个没什么好说的,因为被 static 修饰的 字段是有静态域的,而序列化保存的对象状态是非类的状态,理所当然的会忽视掉static

第二点就是,因为 transient 的作用,就是专门用来隔离不想被序列化字段的关键字。
比如,我们之前的 uesr 类中 存在一个字段叫 pwd,但我们现在不需要它序列化在 info.txt 中,可以加上 transient:

public class User implements Serializable {
    private String name;
    private transient String pwd;
    // 后续 添加
    private Integer id;
    private static final long serialVersionUID = -4232658600822850859L;


    @Override
    public String toString() {
        return "user{" +
                "name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }
    // 此处省略
 }

运行结果为:
添加transient关键字后

实现Externalzable 接口

上面也介绍了 Externalzable 这里不再赘述了,直接上代码:
User2:

public class User2 implements Externalizable {
    private String name;
    private String pwd;


    public User2() {
    }

    public User2(String name, String pwd) {
        this.name = name;
        this.pwd = pwd;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeObject(pwd);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.pwd = (String) in.readObject();
    }
    // 此处省略
 }

运行结果:
Externalzable结果
序列化对象的时候,由于这个类实现了 Externalizable 接口,在 writeExternal() 方法里定义了哪些属性可以序列化,哪些不可以序列化,所以对象在经过这里时,就会把 规定能被序列化的字段 保存文件,不能序列化的不处理,然后在反序列的时候自动调用readExternal()方法,根据序列顺序挨个读取进行反序列,并自动封装成对象返回,然后在类中接收,完成了反序列,注意,就算你的 readExternal 写反了字段,它也会根据序列化类中的排序,自动排序。

为什么说序列化不能将静态字段序列化(补)

1. 序列化的本质

序列化处理的是对象实例的状态,而静态字段属于类级别的状态

  1. 实例字段:每个对象独有的数据(被序列化)
  2. 静态字段:类所有实例共享的数据(不被序列化)
public class User implements Serializable {
    private String name;      // 实例字段 - 会被序列化
    private static int count; // 静态字段 - 不会被序列化
}

2. 技术实现原因

2.1 内存模型角度
  • 静态字段存储在JVM的方法区(Method Area),而不是堆内存
  • 序列化只处理堆内存中的对象实例数据
2.2 序列化机制
  • ObjectOutputStream 只写入对象的非静态非瞬态字段
  • 反序列化时会调用类的无参构造函数,然后恢复实例字段值
  • 静态字段的值由类加载器初始化,不受反序列化影响

3. 设计合理性

3.1 数据一致性

如果允许序列化静态字段:

User u1 = new User();
User.count = 100;
// 序列化u1

User u2 = new User();
User.count = 200;
// 反序列化u1会错误地将count改回100

这将破坏静态字段"类级别共享"的特性。

3.2 安全性考虑

静态字段可能包含:

  • 敏感的系统级配置
  • 运行时的上下文信息
  • 缓存数据

序列化这些内容可能导致安全问题或不一致状态

4. 实际验证

可以通过代码验证静态字段不会被序列化:

public class SerializationTest {
    static class Data implements Serializable {
        static String staticField = "原始静态值";
        String instanceField = "原始实例值";
    }

    public static void main(String[] args) throws Exception {
        // 修改静态字段
        Data.staticField = "修改后的静态值";
        
        // 序列化
        Data original = new Data();
        original.instanceField = "修改后的实例值";
        
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        new ObjectOutputStream(bos).writeObject(original);
        
        // 再次修改静态字段
        Data.staticField = "反序列化前的静态值";
        
        // 反序列化
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
        Data deserialized = (Data) new ObjectInputStream(bis).readObject();
        
        System.out.println("静态字段: " + Data.staticField); 
        // 输出: 反序列化前的静态值 (证明未被序列化影响)
        
        System.out.println("实例字段: " + deserialized.instanceField); 
        // 输出: 修改后的实例值 (证明被序列化)
    }
}

5. 替代方案

如果需要持久化静态字段,可以:

  1. 显式处理:
	class MyClass implements Serializable {
    private static String staticData;
    
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeObject(staticData); // 手动写入
    }
    
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        staticData = (String) ois.readObject(); // 手动读取
    }
}
  1. 使用其他持久化方式:
    • 配置文件(Properties/XML/JSON)
    • 数据库存储
    • 环境变量

小结

Java选择不序列化静态字段是因为:

  1. 静态字段属于类而非对象实例

  2. 保持JVM类加载机制的一致性

  3. 避免共享状态被意外修改

  4. 防止潜在的安全问题

现在看着自己写的好丑啊,但是我不想大改~

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐