Java之对象序列化

本文详细介绍了Java对象序列化的过程,包括实现Serializable接口、对象流序列化、自定义序列化方法,如transient关键字、writeObject和readObject方法、writeReplace和readResolve方法,以及实现Externalizable接口的方式。此外,还探讨了序列化版本号的重要性和处理方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、简介

对象序列化的目的是将对象保存到磁盘, 或者允许在网络中传输。相对的, 反序列化就是根据磁盘中保存的文件恢复对象。

要让某个类的对象能够序列化, 需要实现下面2个接口之一:
-Serializable
-Externalizable

二、对象流实现序列化

通过实现Serializable接口让一个类的对象可序列化的步骤十分简单, 仅仅是在声明一个类时声明实现该接口即可, 无需实现任何方法

1. 成员变量是基本类型
// 用于测试的对象

class Animal implements Serializable {

    private String name;
    private double weight;

    public Animal(String name, double weight) {
        this.name = name;
        this.weight = weight;
    }

    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", weight=" + weight +
                '}';
    }
}

将一个对象输出到磁盘, 可使用对象流 java.io.ObjectOutputStream

String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
    Animal cat = new Animal("cat", 10.2);
    out.writeObject(cat);
}

将一个对象从磁盘恢复可使用对象流 java.io.ObjectInputStream

 String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
     Animal cat = (Animal)ois.readObject();
     System.out.println(cat.toString());
 }
 
// 输出:
Animal{name='cat', weight=10.2}

实现Serializable接口的对象反序列化通过构造器初始化对象?
当从输入流读取对象时, 并没有看到程序调用构造器, 因此反序列化并不需要通过构造器来初始化对象。

代码:略.

2.成员变量是引用类型

若一个类的成员变量是引用类型, 那么该成员变量也必须是可序列化的

// 用于测试的类1
class Animal implements Serializable {
    private String name;
    private double weight;
    private Friend friend;

    public Animal(String name, double weight, Friend friend) {
        this.name = name;
        this.weight = weight;
        this.friend = friend;
    }
    @Override
    public String toString() {
        return "Animal{" +
                "name='" + name + '\'' +
                ", weight=" + weight +
                ", friend=" + friend +
                '}';
    }
}

// 用于测试的类2
class Friend implements Serializable{
    private String name;
    public Friend(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Friend{" +
                "name='" + name + '\'' +
                '}';
    }
}
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
    // 创建对象
    Friend friend = new Friend("Ber");
    Animal cat = new Animal("cat", 10.2, friend);
    // 将对象写入输出流
    out.writeObject(cat);
}

从输入流中恢复对象的代码没有变化

// 输出结果
Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}
3. 序列化多个对象

当保存多个对象并且恢复时, 读取的顺序与保存的顺序需要一致, 而且可以看到, 用于读取对象的方法readObject()并没有参数。

//  多个对象序列化
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
        try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
            // 创建对象
            Friend friend = new Friend("Ber");
            Animal cat = new Animal("cat", 10.2, friend);
            // 将对象1写入输出流
            out.writeObject(cat);
            // 将对象2写入输出流
            out.writeObject(friend);
        }
// 反序列化
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            // 从输入流获取对象1
            Animal cat = (Animal)ois.readObject();
            // 从输入流获取对象2
            Friend friend = (Friend) ois.readObject();

            System.out.println(cat.toString());
            System.out.println(friend.toString());
        }

// 输出:
Animal{name='cat', weight=10.2, friend=Friend{name='Ber'}}
Friend{name='Ber'}

当一个对象被多次序列化时, 是否会输出多个对象?

当多次对同一个对象序列化时, 只有首次的序列化将表示对象的字节序列写到输出流, 后面在进行序列化时, 被写入输出流的只是序列化编号, 从而保证是从字节序列恢复的是同一个对象。

// 序列化代码片段
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
            // 创建对象
            Friend friend = new Friend("Ber");
            // 将对象写入输出流
            out.writeObject(friend);
            // 将对象再次写入输出流
            out.writeObject(friend);
        }
// 反序列化代码片段

try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
            // 从输入流获取对象1
            Friend friend = (Friend) ois.readObject();
            // 从输入流获取对象2
            Friend friend2 = (Friend) ois.readObject();
            System.out.println(friend == friend2);
        }

// 输出:
true

当一个对象被列化后, 修改该对象属性并再次序列化, 那么反序列化的结果是什么?
反序列化的结果都是首次序列化的属性值, 而这也符合了多次序列化对象的规则: 只有对象的首次序列化才会将表示对象的字节流写入输出流, 后面的调用只会写入序列化编号。

// 对象序列化片段

// 创建对象
Friend friend = new Friend("Ber");
// 将对象写入输出流
out.writeObject(friend);

//修改对象属性值并写入输出流
friend.setName("Hos");
out.writeObject(friend);
// 对象反序列化片段

// 从输入流获取对象
Friend friend = (Friend) ois.readObject();
Friend friend2 = (Friend) ois.readObject();

System.out.println(friend.toString());
System.out.println(friend2.toString());


// 输出:
Friend{name='Ber'}
Friend{name='Ber'}

三、自定义序列化(一)

1. transient关键字

在默认情况下, 将对象序列化时会将其所有的实例变量进行实例化。 若成员变量是引用类型, 则引用的对象也会被实例化。 假设想要忽略对某个对实例变量的序列化, 可使用transient关键字。

// 用于测试的类

class Friend implements Serializable{
    private String name;
    private transient int age;

    public Friend(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Friend{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 对象序列化片段
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
    // 创建对象
    Friend friend = new Friend("Ber", 20);
    // 将对象写入输出流
    out.writeObject(friend);
}
/ / 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
   // 从输入流获取对象
    Friend friend = (Friend) ois.readObject();
    System.out.println(friend.toString());
}

输出:
Friend{name='Ber', age=0}

可以看到, 被transient修饰的成员变量age在反序列化后并不是序列化时的值。

2. 目标类提供writeObject、readObject方法
methoddesc
private void readObject(ObjectInputStream in)从输入流中读取对象
private void writeObject(ObjectOutputStream out)将对象写到输出流
private void readObjectNoData()当序列化流不完整, 正确的初始化反序列化对象
class Friend implements Serializable{

    private String name;
    private transient int age;

    public Friend(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

序列化和反序列化的代码与3.1相同, 没有变化。

首先, 通过提供writeObject方法, 我们可以决定序列化哪些变量, 以及如何序列化变量。
其次, 提高了在通过网络传输字节化序列时的安全性。

注意:
(1). readObject恢复实例变量的顺序必须与writeObject存储实例变量一致。
(2). readObject恢复实例变量的方式必须与writeObject存储实例变量的方式相对应. 在上例中:
writeObject序列化String类型变量: String —> StringBuffer —> StringBuffer.reverse
readObject反序列化String类型变量: (StringBuffer)Object —> StringBuffer.reverse —> toString

3. 目标类提供writeReplace方法

使用该方法时, 会将原本将要序列化的对象改为对另一个对象的序列化。

Java序列化保护机制保证: 在序列化对象之前, 先调用该对象的writeReplace()方法, 若该方法返回的是另一个Java对象, 则转为序列化该对象。

// 用于测试的类
class Friend implements Serializable {

    private String name;
    private transient int age;

    public Friend(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private Object writeReplace() {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}
// 序列化对象代码
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
   // 创建对象
    Friend friend = new Friend("Ber", 50);
    // 将对象写入输出流
    out.writeObject(friend);
}
// 反序列化代码
 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
   // 从输入流获取对象
    ArrayList list = (ArrayList)ois.readObject();
    System.out.println(list);
}

// 输出:
[Ber, 50]

可以看到, 在将对象序列化代码中无需改变, 但是在反序列化时需要将对象转为与writeReplace方法中定义的类型(ArrayList)一致。因此可以知道, 对原对象的序列化实际上转为了对ArrayList对象的序列化。

4. 目标类提供readResolve()方法

writeReplace方法是在序列化时替换序列化的对象, 那么与之相对的, readResolve()方法的返回值会替换原本反序列化的对象, 因为readResolve()会在调用readObject()时被调用。
在这里插入图片描述

// 用于测试的bean

class Friend implements Serializable {
    private String name;
    private transient int age;

    public Friend(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 提供readResolve方法
    private Object readResolve() {
        return String.valueOf("I am a String");
    }

    @Override
    public String toString() {
        return "Friend{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 序列化对象
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
    // 创建对象
    Friend friend = new Friend("Ber", 50);
    // 将对象写入输出流
    out.writeObject(friend);
}
// 反序列化对象
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
  // 从输入流获取对象
   Object object = ois.readObject();
   System.out.println(object.toString());
}

// 输出:
I am a String

根据例子可知, Friend对象反序列化后返回的对象是readResolve()中定义的String对象。

注意:
与writeReplace()方法相似, readResolve()方法可以使用任意的访问控制符。 假如readResolve()不使用 private 或final 修饰, 那么就会有被子类继承的风险, 也就意味着假如子类继承了这个方法且没有重写, 那么对子类反序列化时将会得到一个父类对象。 但总是让子类重写方法也是一个负担, 若是需要重写, 则最好使用private或final修饰。

四、自定义序列化(二)

实现Externalizable接口

与实现Serializable接口不同的是, 实现Externalizable接口需要做2件事:
(1) 实现 writeExternal(ObjectOutput out) 和 readExternal(ObjectInput in) 方法。
(2) 为该类提供无参数构造器, 无参数构造器在反序列化时被使用到。

除此之外, 序列化反序列化的代码都是调用ObjectOutputStream的writeObject方法ObjectInputStream的readObject()方法完成。

class Person implements Externalizable {
    private String name;
    private int age;

    public Person() {
        System.out.println("无参构造器调用");
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 实现接口方法
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
        out.writeInt(age);
    }

    // 实现接口方法
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        this.name = (String) in.readObject();
        this.age = in.readInt();
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
// 序列化代码
try(ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(filename))){
   // 创建对象
    Person person = new Person("Ber", 50);
    // 将对象写入输出流
    out.writeObject(person);
}
// 反序列化代码
String filename = "C:\\Users\\peter1950\\Desktop\\T\\myobj.txt";
 try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename))) {
     // 从输入流获取对象
     Person person = (Person) ois.readObject();
     System.out.println(person.toString());
 }
 
输出:
无参构造器调用
Person{name='Ber', age=50}

Serializable和Externalizable接口不同之处:

SerializableExternalizable
自动存储对象信息自定义存储对象的信息
易于实现, 只需声明实现该接口即可需要实现接口的方法、提供无参构造器
性能略差性能略好

Externalizable接口虽然能带来性能上的提升, 但是也增加了编程的复杂度, 因此大多数情况下使用实现Serializable的方法。


五、小结

  1. 对象的类名成员变量都会被序列化, 方法、静态变量、transient修饰的成员变量不会被序列化
  2. 实现Serializable接口的类可以使用transient修饰成员变量, 让该成员变量不会被序列化, 虽然使用static修饰成员变量也能达到这个效果, 但是不推荐这样使用。
  3. 要进行序列化的对象的成员变量也必须是可序列化的, 假如该成员变量是不可序列化的, 需要使用transient修饰, 否则该类对象时不可序列化的。
  4. 反序列化必须要有序列化对象的class文件
  5. 当通过文件、网络来进行反序列化时, 必须按实际写入的顺序读取。

六、序列化版本

当进行对象反序列化时需要提供对象的class文件, 那么当class文件的版本升级时, 如何保证两个class文件的兼容性?
通过在序列化类显式的提供一个private static final long serialVersionUID值。 这个值用于标识java类的序列化版本, 也就是说, 即使一个类升级后, 只要它的serialVersionUID不变, 序列化机制也会把它们当成同一版本。

该值获取方法:
(1) 这个值可以自己定
(2) 使用bin目录下的serialver.exe工具来获取该值. 命令的格式为serialver 类名:
在这里插入图片描述
(3)通过该开发工具生成
IDEA如何自动生成 serialVersionUID 的设置

假如不显式的提供serialVersionUID会发生什么情况?
(1) 该值由JVM根据类的信息进行计算, 一个类修改后计算出的值往往会发生变化, 造成反序列化因为类版本不兼容而失败。
(2) 不利于在不同的JVM进行移植, 不同的JVM可能会有不同的计算策略, 因此一个类虽然没有改变, 但是会因为序列化版本不兼容造成无法反序列化的现象。

假如一个类的修改导致了反序列化失败, 则应该重新分配serialVersionUID的值。

### 解决PyCharm无法加载Conda虚拟环境的方法 #### 配置设置 为了使 PyCharm 能够成功识别并使用 Conda 创建的虚拟环境,需确保 Anaconda 的路径已正确添加至系统的环境变量中[^1]。这一步骤至关重要,因为只有当 Python 解释器及其关联工具被加入 PATH 后,IDE 才能顺利找到它们。 对于 Windows 用户而言,在安装 Anaconda 时,默认情况下会询问是否将它添加到系统路径里;如果当时选择了否,则现在应该手动完成此操作。具体做法是在“高级系统设置”的“环境变量”选项内编辑 `Path` 变量,追加 Anaconda 安装目录下的 Scripts 文件夹位置。 另外,建议每次新建项目前都通过命令行先激活目标 conda env: ```bash conda activate myenvname ``` 接着再启动 IDE 进入工作区,这样有助于减少兼容性方面的问题发生概率。 #### 常见错误及修复方法 ##### 错误一:未发现任何解释器 症状表现为打开 PyCharm 新建工程向导页面找不到由 Conda 构建出来的 interpreter 列表项。此时应前往 Preferences/Settings -> Project:...->Python Interpreter 下方点击齿轮图标选择 Add...按钮来指定自定义的位置。按照提示浏览定位到对应版本 python.exe 的绝对地址即可解决问题。 ##### 错误二:权限不足导致 DLL 加载失败 有时即使指定了正确的解释器路径,仍可能遇到由于缺乏适当的操作系统级许可而引发的功能缺失现象。特别是涉及到调用某些特定类型的动态链接库 (Dynamic Link Library, .dll) 时尤为明显。因此拥有管理员身份执行相关动作显得尤为重要——无论是从终端还是图形界面触发创建新 venv 流程均如此处理能够有效规避此类隐患。 ##### 错误三:网络连接异常引起依赖下载超时 部分开发者反馈过因网速慢或者其他因素造成 pip install 操作中途断开进而影响整个项目的初始化进度条卡住的情况。对此可尝试调整镜像源加速获取速度或是离线模式预先准备好所需资源包后再继续后续步骤。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值