对象序列化与反序列化:`Serializable` 接口与 `transient` 关键字

对象序列化与反序列化:Serializable 接口与 transient 关键字——一场数据的穿越之旅

各位看官,大家好!今天咱们聊聊Java世界里一个既神秘又实用的技术——对象序列化和反序列化。这玩意儿听起来高深莫测,仿佛魔法一般,其实说白了,就是把咱们辛辛苦苦创建的对象,从“活蹦乱跳”的内存状态,变成一堆“死气沉沉”的字节,方便我们存储到硬盘里,或者通过网络传给远方的朋友。等需要用的时候,再把这些字节“复活”,还原成原来的对象。

是不是有点像科幻电影里的瞬间移动?没错,对象序列化和反序列化,就是数据对象的“穿越之旅”。而这场旅行的“通行证”和“安检员”,就是咱们今天要重点介绍的 Serializable 接口和 transient 关键字。

一、什么是对象序列化和反序列化?

想象一下,你写了一个游戏,里面有个角色叫做“小明”,他有名字、等级、血量等等属性。你玩了一下午,好不容易把小明升到了满级,血量也加满了。现在你想关机睡觉了,明天再接着玩。问题来了,关机后,内存里的数据就没了,明天重新打开游戏,小明又变成了一级菜鸟,血量也空了,这可咋办?

这时候,对象序列化就派上用场了。它可以把小明的对象,包括他的名字、等级、血量等属性,转换成一串字节,存到硬盘上的一个文件里。第二天,你再打开游戏,程序就可以读取这个文件,把小明的对象“复活”,让他恢复到昨天的满级状态。

简单来说:

  • 序列化 (Serialization): 将对象的状态信息转换为可以存储或传输的形式的过程。通常是将对象转换为字节流。
  • 反序列化 (Deserialization): 将存储或传输的字节流转换为对象的过程。是序列化的逆过程。

序列化的用途:

  • 持久化存储: 将对象保存到硬盘上,比如保存游戏进度、配置信息等。
  • 网络传输: 通过网络将对象传递给其他应用程序,比如远程方法调用 (RMI)、Web Services 等。
  • 缓存: 将对象缓存到内存中,提高访问速度。

二、Serializable 接口:对象的“通行证”

在Java中,要让一个对象能够被序列化,必须实现 Serializable 接口。这个接口非常特殊,它里面没有任何方法需要实现。它就像一个“通行证”,告诉Java虚拟机 (JVM):”这个类的对象可以被序列化。“

示例代码:

import java.io.Serializable;

public class Hero implements Serializable {
    private String name;
    private int level;
    private int hp;

    public Hero(String name, int level, int hp) {
        this.name = name;
        this.level = level;
        this.hp = hp;
    }

    // Getter 和 Setter 方法省略

    @Override
    public String toString() {
        return "Hero{" +
                "name='" + name + ''' +
                ", level=" + level +
                ", hp=" + hp +
                '}';
    }

    public static void main(String[] args) {
        Hero hero = new Hero("小明", 100, 1000);
        System.out.println(hero); // 输出:Hero{name='小明', level=100, hp=1000}
    }
}

上面的代码中,Hero 类实现了 Serializable 接口,这意味着我们可以对 Hero 类的对象进行序列化和反序列化操作。

如何序列化和反序列化对象?

Java提供了 ObjectOutputStreamObjectInputStream 类来实现对象的序列化和反序列化。

  • ObjectOutputStream: 用于将对象写入到输出流中。
  • ObjectInputStream: 用于从输入流中读取对象。

示例代码:

import java.io.*;

public class SerializationExample {
    public static void main(String[] args) {
        // 序列化
        Hero hero = new Hero("小明", 100, 1000);
        try (FileOutputStream fileOut = new FileOutputStream("hero.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(hero);
            System.out.println("对象已序列化到 hero.ser");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        Hero deserializedHero = null;
        try (FileInputStream fileIn = new FileInputStream("hero.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
            deserializedHero = (Hero) in.readObject();
            System.out.println("对象已反序列化");
            System.out.println(deserializedHero); // 输出:Hero{name='小明', level=100, hp=1000}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

上面的代码首先创建了一个 Hero 对象,然后使用 ObjectOutputStream 将其序列化到名为 hero.ser 的文件中。接着,使用 ObjectInputStream 从文件中读取字节流,并将其反序列化为 Hero 对象。最终,打印反序列化后的 Hero 对象,可以看到它的属性值与序列化之前完全一致。

注意事项:

  • 如果一个类实现了 Serializable 接口,其所有成员变量也必须是可序列化的。这意味着,成员变量要么是基本数据类型,要么是实现了 Serializable 接口的类。
  • 如果一个类的成员变量是不可序列化的,那么在序列化该类的对象时,会抛出 NotSerializableException 异常。
  • 静态成员变量 (static) 不会被序列化,因为它们属于类,而不是对象。
  • 反序列化时,需要保证类的 class 文件是可用的。如果找不到 class 文件,会抛出 ClassNotFoundException 异常。

三、transient 关键字:对象的“秘密”

有时候,我们希望对象的某些属性不要被序列化。比如,密码、密钥等敏感信息,或者一些在反序列化后需要重新计算的值。这时候,transient 关键字就派上用场了。

transient 关键字用于修饰类的成员变量,表示该成员变量不参与序列化过程。也就是说,在序列化对象时,被 transient 修饰的成员变量的值会被忽略,反序列化后,该成员变量的值会被设置为默认值(例如,int类型的默认值为0,String类型的默认值为null)。

示例代码:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 不参与序列化

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Getter 和 Setter 方法省略

    @Override
    public String toString() {
        return "User{" +
                "username='" + username + ''' +
                ", password='" + password + ''' +
                '}';
    }
}

在上面的代码中,password 成员变量被 transient 修饰,这意味着在序列化 User 对象时,password 的值不会被保存。反序列化后,password 的值将为 null

示例代码:

import java.io.*;

public class TransientExample {
    public static void main(String[] args) {
        // 序列化
        User user = new User("张三", "123456");
        try (FileOutputStream fileOut = new FileOutputStream("user.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            out.writeObject(user);
            System.out.println("对象已序列化到 user.ser");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        User deserializedUser = null;
        try (FileInputStream fileIn = new FileInputStream("user.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
            deserializedUser = (User) in.readObject();
            System.out.println("对象已反序列化");
            System.out.println(deserializedUser); // 输出:User{username='张三', password='null'}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

可以看到,反序列化后的 User 对象,password 的值为 null

transient 的使用场景:

  • 保护敏感信息: 比如密码、密钥等,不希望被保存到硬盘上或通过网络传输。
  • 优化序列化过程: 某些成员变量的值可以在反序列化后重新计算得到,不需要保存,可以减少序列化的大小和时间。
  • 避免循环引用: 如果对象之间存在循环引用,可能会导致序列化过程陷入无限循环。可以使用 transient 关键字来打破循环引用。

四、serialVersionUID:对象的“身份证”

在进行反序列化时,JVM会检查序列化对象的 serialVersionUID 是否与当前类的 serialVersionUID 相同。如果不同,会抛出 InvalidClassException 异常。

serialVersionUID 是一个长整型的静态常量,用于标识类的版本。它的作用是:

  • 保证反序列化的兼容性: 当类的结构发生改变时,如果没有修改 serialVersionUID,那么在反序列化之前序列化的对象时,可能会出现错误。通过修改 serialVersionUID,可以显式地表示类的版本发生了改变,从而避免反序列化错误。
  • 控制反序列化过程: 可以通过自定义 serialVersionUID,来控制反序列化过程。

如何生成 serialVersionUID

有两种方式生成 serialVersionUID

  1. 默认生成: 如果类没有显式地定义 serialVersionUID,JVM会根据类的结构自动生成一个 serialVersionUID。但是,这种方式生成的 serialVersionUID 可能会因为类的微小改变而发生变化,从而导致反序列化失败。
  2. 显式定义: 建议显式地定义 serialVersionUID,并将其设置为一个固定的值。可以使用 IDE 自动生成 serialVersionUID

示例代码:

import java.io.Serializable;

public class Person implements Serializable {
    private static final long serialVersionUID = 1L; // 显式定义 serialVersionUID
    private String name;
    private int age;

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

    // Getter 和 Setter 方法省略

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

何时需要修改 serialVersionUID

当类的结构发生重大改变时,需要修改 serialVersionUID。比如:

  • 添加或删除成员变量。
  • 修改成员变量的类型。
  • 修改类的继承关系。

注意事项:

  • serialVersionUID 必须是 private static final 的。
  • 如果类的结构发生了改变,但是没有修改 serialVersionUID,可能会导致反序列化失败。

五、自定义序列化和反序列化

有时候,默认的序列化和反序列化过程可能无法满足我们的需求。比如,我们可能需要对某些成员变量进行加密后再序列化,或者在反序列化后对对象进行一些额外的处理。这时候,我们可以通过实现 writeObject()readObject() 方法来自定义序列化和反序列化过程。

writeObject() 方法:

用于自定义序列化过程。在该方法中,可以编写自定义的序列化逻辑,例如对敏感信息进行加密。

readObject() 方法:

用于自定义反序列化过程。在该方法中,可以编写自定义的反序列化逻辑,例如对加密的信息进行解密,或者对对象进行一些额外的初始化操作。

示例代码:

import java.io.*;

public class Secret implements Serializable {
    private static final long serialVersionUID = 1L;
    private String data;
    private transient String encryptedData; // 不参与序列化

    public Secret(String data) {
        this.data = data;
    }

    // Getter 和 Setter 方法省略

    // 自定义序列化方法
    private void writeObject(ObjectOutputStream out) throws IOException {
        // 加密数据
        encryptedData = encrypt(data);
        // 先调用默认的序列化方法
        out.defaultWriteObject();
        // 写入加密后的数据
        out.writeObject(encryptedData);
    }

    // 自定义反序列化方法
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 先调用默认的反序列化方法
        in.defaultReadObject();
        // 读取加密后的数据
        encryptedData = (String) in.readObject();
        // 解密数据
        data = decrypt(encryptedData);
    }

    // 加密方法 (示例)
    private String encrypt(String data) {
        return "encrypted_" + data;
    }

    // 解密方法 (示例)
    private String decrypt(String encryptedData) {
        return encryptedData.substring(10);
    }

    @Override
    public String toString() {
        return "Secret{" +
                "data='" + data + ''' +
                ", encryptedData='" + encryptedData + ''' +
                '}';
    }
}

在上面的代码中,Secret 类实现了 writeObject()readObject() 方法,用于自定义序列化和反序列化过程。在 writeObject() 方法中,首先对 data 进行加密,然后将加密后的数据写入到输出流中。在 readObject() 方法中,首先从输入流中读取加密后的数据,然后对其进行解密,还原成原始的 data

注意事项:

  • writeObject()readObject() 方法必须是 private 的,并且没有返回值。
  • writeObject() 方法中,必须先调用 out.defaultWriteObject() 方法,再写入自定义的数据。
  • readObject() 方法中,必须先调用 in.defaultReadObject() 方法,再读取自定义的数据。
  • writeObject()readObject() 方法可能会抛出 IOExceptionClassNotFoundException 异常,需要进行处理。

六、总结:对象序列化和反序列化的“葵花宝典”

对象序列化和反序列化是Java中一个非常重要的技术,它可以帮助我们实现对象的持久化存储和网络传输。掌握 Serializable 接口和 transient 关键字,以及自定义序列化和反序列化方法,可以让我们更好地控制对象的序列化过程,保护敏感信息,优化序列化性能。

表格总结:

特性 描述 作用
序列化 将对象的状态信息转换为可以存储或传输的形式的过程。 持久化存储、网络传输、缓存。
反序列化 将存储或传输的字节流转换为对象的过程。 恢复对象的状态。
Serializable 接口,没有任何方法需要实现,用于标识类可以被序列化。 允许对象进行序列化和反序列化。
transient 关键字,用于修饰类的成员变量,表示该成员变量不参与序列化过程。 保护敏感信息,优化序列化过程,避免循环引用。
serialVersionUID 长整型的静态常量,用于标识类的版本。 保证反序列化的兼容性,控制反序列化过程。
writeObject() 方法,用于自定义序列化过程。 对敏感信息进行加密,或者进行其他自定义的序列化逻辑。
readObject() 方法,用于自定义反序列化过程。 对加密的信息进行解密,或者对对象进行一些额外的初始化操作。

希望这篇文章能够帮助你更好地理解对象序列化和反序列化,并将其应用到实际开发中。记住,技术并非高不可攀,只要用心学习,人人都可以成为编程高手! 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注