Java 中的序列化与反序列化

Java 中的​​序列化(Serialization)​​是将对象转换为字节流的过程,用于持久化存储(如写入文件、数据库)或网络传输(如RPC调用);​​反序列化(Deserialization)​​则是将字节流恢复为对象的过程。Serializable 接口是 Java 提供的标记接口(无方法),用于标识一个类的实例可以被序列化。以下是详细说明、核心规则及典型示例。

一、核心概念与作用

1. 序列化与反序列化
  • ​序列化​​:将 Java 对象转换为二进制字节流(byte[]),可存储到文件、数据库或通过网络传输。
  • ​反序列化​​:将二进制字节流恢复为 Java 对象,实现对象的重建。
2. Serializable 接口
  • ​标记接口​​:仅用于标识类的实例可被序列化,无实际方法。若类未实现此接口,序列化时会抛出 NotSerializableException
  • ​默认行为​​:JVM 会根据类的字段(除 static 和 transient 修饰的字段外)自动生成序列化逻辑。

二、核心规则与注意事项

1. transient 关键字
  • ​作用​​:修饰字段时,该字段不会被序列化(反序列化后值为默认值:null0false 等)。
  • ​场景​​:用于敏感数据(如密码)或临时数据(无需持久化)。
2. static 字段
  • 静态字段属于类而非对象,​​不会被序列化​​(反序列化后恢复为类的当前静态值)。
3. serialVersionUID 版本号
  • ​作用​​:标识序列化类的版本。反序列化时,JVM 会检查字节流的 serialVersionUID 是否与当前类的 serialVersionUID 一致:
    • 一致:允许反序列化;
    • 不一致:抛出 InvalidClassException(如类的字段增删、类型修改)。
  • ​建议​​:显式声明 serialVersionUID(避免因类结构变化导致反序列化失败)。
4. 自定义序列化逻辑
  • 若需要控制序列化/反序列化的细节(如加密、压缩),可通过重写 writeObject(ObjectOutputStream out) 和 readObject(ObjectInputStream in) 方法实现。

三、典型示例

示例1:基础对象序列化与反序列化

定义一个可序列化的 User 类,演示对象写入文件并读取恢复的过程。

​步骤1:定义可序列化的类​
显式声明 serialVersionUID,并包含普通字段、transient 字段和 static 字段。

java
复制
import java.io.Serializable;

public class User implements Serializable {
    // 显式声明 serialVersionUID(推荐)
    private static final long serialVersionUID = 1L;

    private String name;      // 普通字段(会被序列化)
    private transient int age; // transient 字段(不会被序列化)
    private static String role = "guest"; // static 字段(不会被序列化)

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

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

​步骤2:序列化对象到文件​
使用 ObjectOutputStream 将 User 对象写入文件。

java
复制
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;

public class SerializeDemo {
    public static void main(String[] args) {
        User user = new User("Alice", 25);

        // 序列化:对象 → 字节流 → 文件
        try (FileOutputStream fos = new FileOutputStream("user.ser");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(user); // 写入对象
            System.out.println("序列化完成,对象:" + user);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

​步骤3:从文件反序列化对象​
使用 ObjectInputStream 从文件读取字节流并恢复为 User 对象。

java
复制
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.IOException;

public class DeserializeDemo {
    public static void main(String[] args) {
        // 反序列化:文件 → 字节流 → 对象
        try (FileInputStream fis = new FileInputStream("user.ser");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            User restoredUser = (User) ois.readObject(); // 读取对象
            System.out.println("反序列化完成,恢复的对象:" + restoredUser);
            // 输出说明:age 是 transient 字段(值为0),role 是 static 字段(值为当前类的 role)
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

​输出结果​​:

复制
序列化完成,对象:User{name='Alice', age=25, role='guest'}
反序列化完成,恢复的对象:User{name='Alice', age=0, role='guest'}
  • age 因 transient 修饰,反序列化后为默认值 0
  • role 因 static 修饰,反序列化后恢复为类的当前静态值(未被序列化)。
示例2:包含引用对象的序列化

若对象包含其他可序列化对象的引用,序列化会递归处理所有关联对象(需确保所有关联对象也实现 Serializable)。

​定义 Address 类(可序列化)​​:

java
复制
import java.io.Serializable;

public class Address implements Serializable {
    private static final long serialVersionUID = 1L;
    private String city;

    public Address(String city) {
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address{city='" + city + "'}";
    }
}

​修改 User 类(包含 Address 引用)​​:

java
复制
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private transient int age;
    private Address address; // 引用可序列化对象

    public User(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

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

​序列化与反序列化测试​​:

java
复制
public class SerializeWithReferenceDemo {
    public static void main(String[] args) {
        Address addr = new Address("Beijing");
        User user = new User("Bob", 30, addr);

        // 序列化
        try (FileOutputStream fos = new FileOutputStream("user_with_ref.ser");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (FileInputStream fis = new FileInputStream("user_with_ref.ser");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            User restoredUser = (User) ois.readObject();
            System.out.println("恢复的对象:" + restoredUser);
            // 输出:User{name='Bob', age=0, address=Address{city='Beijing'}}
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  • 关联的 Address 对象会被递归序列化和反序列化,最终恢复完整的对象图。
示例3:自定义序列化逻辑(加密敏感字段)

若需要对敏感字段(如密码)进行加密存储,可通过重写 writeObject 和 readObject 方法自定义序列化过程。

​定义 SecureUser 类(自定义序列化)​​:

java
复制
import java.io.*;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class SecureUser implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // 敏感字段,不直接序列化
    private static final String KEY = "1234567890123456"; // AES 密钥(16字节)

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

    // 自定义序列化逻辑(加密密码)
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // 默认序列化非 transient 字段(username)
        try {
            // 加密密码
            String encryptedPwd = encrypt(password);
            out.writeObject(encryptedPwd); // 写入加密后的密码
        } catch (Exception e) {
            throw new IOException("加密失败", e);
        }
    }

    // 自定义反序列化逻辑(解密密码)
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // 默认反序列化非 transient 字段(username)
        try {
            // 读取加密的密码并解密
            String encryptedPwd = (String) in.readObject();
            this.password = decrypt(encryptedPwd);
        } catch (Exception e) {
            throw new IOException("解密失败", e);
        }
    }

    // AES 加密
    private String encrypt(String data) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes());
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

    // AES 解密
    private String decrypt(String encryptedData) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, keySpec);
        byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(decryptedBytes);
    }

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

​测试自定义序列化​​:

java

复制

public class CustomSerializeDemo {
    public static void main(String[] args) {
        SecureUser user = new SecureUser("admin", "p@ssw0rd");

        // 序列化
        try (FileOutputStream fos = new FileOutputStream("secure_user.ser");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(user);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 反序列化
        try (FileInputStream fis = new FileInputStream("secure_user.ser");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            SecureUser restoredUser = (SecureUser) ois.readObject();
            System.out.println("恢复的对象:" + restoredUser); 
            // 输出:SecureUser{username='admin', password='p@ssw0rd'}(密码被正确解密)
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  • 敏感字段 password 被加密后存储,反序列化时自动解密,避免明文泄露。

四、常见问题与最佳实践

1. NotSerializableException
  • ​原因​​:类未实现 Serializable 接口。
  • ​解决​​:让类实现 Serializable 接口。
2. 版本号不一致(InvalidClassException
  • ​原因​​:序列化时的类 serialVersionUID 与反序列化时的类 serialVersionUID 不同(如类的字段增删)。
  • ​解决​​:显式声明 serialVersionUID(如 private static final long serialVersionUID = 1L;),避免自动生成。
3. 敏感数据泄露
  • ​原因​​:transient 字段虽不被序列化,但内存中的对象可能被反射获取;或默认序列化可能暴露敏感信息。
  • ​解决​​:对敏感字段使用 transient 修饰,并结合自定义序列化逻辑(如加密)。
4. 静态变量与单例模式
  • ​静态变量​​:不属于对象,序列化不影响其值;反序列化后恢复为类的当前静态值。
  • ​单例模式​​:若单例类可序列化,反序列化时会生成新实例,破坏单例。需通过 readResolve() 方法避免:
    java
    复制
    protected Object readResolve() throws ObjectStreamException {
        return getInstance(); // 返回现有单例实例,而非新建
    }
5. 性能与兼容性
  • ​性能​​:Java 原生序列化效率较低(字节流冗余大),推荐使用 JSON(如 Jackson)、Protobuf 等跨语言序列化方案。
  • ​兼容性​​:尽量保持类的字段稳定(避免删除或修改类型),如需修改,可通过 serialVersionUID 控制版本兼容。

五、总结

​特性​ ​说明​
​核心接口​ Serializable(标记接口,无方法)
​序列化工具​ ObjectOutputStream(序列化)、ObjectInputStream(反序列化)
​关键规则​ transient 字段不序列化;static 字段不序列化;显式声明 serialVersionUID
​自定义逻辑​ 重写 writeObject 和 readObject 方法
​适用场景​ 对象持久化(文件/数据库)、网络传输(RPC/HTTP)
​替代方案​ JSON(Jackson)、Protobuf(跨语言、高性能)

​最佳实践​​:

  • 优先显式声明 serialVersionUID
  • 敏感字段使用 transient 并结合加密;
  • 避免序列化大对象(影响性能);
  • 跨语言场景选择 Protobuf 等通用序列化协议。

你可能感兴趣的:(java,开发语言)