CodeGym /课程 /JAVA 25 SELF /集合序列化中的常见错误解析

集合序列化中的常见错误解析

JAVA 25 SELF
第 44 级 , 课程 4
可用

1. NotSerializableException:当集合无法被序列化时

在序列化集合时最常见且最棘手的错误是 java.io.NotSerializableException。当集合中至少有一个元素未实现接口 Serializable 时就会发生。

来看一个简单的示例:

import java.io.*;
import java.util.*;

class Book {
    String title;
    Book(String title) { this.title = title; }
}

public class LibraryApp {
    public static void main(String[] args) throws Exception {
        List<Book> books = new ArrayList<>();
        books.add(new Book("董贝父子"));

        // 尝试序列化集合
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
            oos.writeObject(books); // 砰!NotSerializableException
        }
    }
}

会发生什么?在 oos.writeObject(books) 这一行,你会得到异常:

java.io.NotSerializableException: Book

为什么?因为类 Book 没有实现接口 Serializable。即使集合本身(ArrayList)支持序列化,集合中的元素也必须是可序列化的!

如何诊断

错误消息中总会指出导致问题的类——在异常信息里找它。如果集合很大,而错误只在特定条件下出现,可能是某个元素被意外加入而没有实现 Serializable

如何修复

在你的类上添加 implements Serializable

class Book implements Serializable {
    String title;
    Book(String title) { this.title = title; }
}

提示:如果集合包含不同类型的对象,请逐一检查它们是否都实现了 Serializable

2. 反序列化时的 ClassCastException:当泛型掉链子

在 Java 中,集合的泛型参数信息在编译后会被擦除(type erasure)。这意味着,如果你序列化的是 List<String>,却按 List<Integer> 反序列化,编译器不会发现错误,但在运行时你会得到 ClassCastException

示例:

// 序列化
List<String> names = Arrays.asList("安娜", "鲍里斯");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
    oos.writeObject(names);
}

// 反序列化(危险!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
    List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
    Integer first = numbers.get(0); // 砰!ClassCastException
}

错误:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

如何避免

  • 不要使用“原始”集合类型(raw types),也不要在没有必要时进行类型强制转换。
  • 如果不确定内容,反序列化后请检查元素类型。
  • 在文档中明确说明序列化的是哪种集合类型,以及读取时的预期类型。

安全反序列化示例:

Object obj = ois.readObject();
if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty() && list.get(0) instanceof String) {
        @SuppressWarnings("unchecked")
        List<String> safeNames = (List<String>) obj; // 警告已被抑制,但类型已检查!
    }
}

3. 类结构变更:serialVersionUID 与向后兼容性

你序列化了一个集合,随后决定在元素类中添加新字段、修改字段名,或者干脆改变类的结构。现在,当你尝试反序列化旧文件时,会得到一个神秘的错误:

java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678

为什么会这样

每个可序列化的类都有一个唯一的版本标识——serialVersionUID。如果类发生了变化(例如你添加了字段),JVM 会计算出新的 serialVersionUID,反序列化时会发现当前类的版本与序列化时的不一致。

如何避免

  • 在你的类中显式声明 serialVersionUID
class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;
    // ...
}
  • 保持向后兼容:如果计划读取旧文件,不要删除或重命名字段。
  • 在更改后测试反序列化。

如果确实需要修改类怎么办?

  • 考虑实现 readObject/writeObject 方法,以手动控制序列化。
  • 或迁移数据:使用旧版本的类读取旧文件,然后以新格式重新保存。

4. 序列化不可变集合时的数据丢失

在较新的 Java 版本中,引入了不可变集合,例如通过 List.of()Set.of()Map.of() 创建的集合。在较旧的 Java 版本(12 之前)以及某些第三方实现中,此类集合的序列化可能不够正确:反序列化后集合变成可变的,或直接发生错误。

示例:

List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
    oos.writeObject(list);
}

在较旧的 JVM 中,反序列化时可能会抛出错误,或者集合不再保持不可变。

如何避免

  • 查阅你所使用 Java 版本的文档。
  • 对这类集合进行序列化与反序列化的测试。
  • 如果需要保持不可变性,反序列化后用 Collections.unmodifiableList(list) 进行包装。

5. transient 与 static 字段的序列化

这些字段会发生什么:

  • transient——用该关键字标记的字段根本不会被序列化。反序列化后它们将具有默认值(例如 null 或 0)。
  • static——类字段(而非对象字段)永远不会被序列化。

示例:

class Book implements Serializable {
    String title;
    transient String cache; // 不会被序列化!
    static String publisher = "Default"; // 也不会被序列化!
}

为什么这很重要

如果你在对象内部保存某些可计算值或缓存,请将它们标记为 transient——这能节省空间并加快序列化速度.

注意:反序列化之后,需要重新计算或重新初始化 transient 字段。

6. 序列化大型集合:性能与文件大小

问题:

  • 大型集合(例如,包含一百万个对象)可能会导致文件巨大、读写时间很长,甚至内存不足(OutOfMemoryError)。
  • 在序列化对象图(例如复杂的相互关联的集合)时,文件大小可能会意外膨胀。

如何避免

  • 分批序列化集合:例如逐个或分小批写入对象。
  • 使用流式处理:不要一次性序列化整个集合,而是按需序列化元素。
  • 对文件进行压缩:使用 GZIPOutputStream 来减小文件大小。

流式序列化示例:

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
    for (Book book : bigList) {
        oos.writeObject(book);
    }
}

注意:采用这种方式时,反序列化需要知道写入了多少个对象(或者使用专门的“结束标记”)。

1
调查/小测验
复杂结构的序列化第 44 级,课程 4
不可用
复杂结构的序列化
复杂结构的序列化
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION