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);
}
}
注意:采用这种方式时,反序列化需要知道写入了多少个对象(或者使用专门的“结束标记”)。
GO TO FULL VERSION