1. 泛型集合的序列化与反序列化
Java 中的泛型(泛化)并不是魔法,而是由编译器维持的一种“错觉”。在编译阶段,关于泛型类型参数的信息会被擦除(这称为 type erasure,即“类型擦除”)。也就是说,在运行时(runtime)List<String> 与 List<Object> 或 List<Integer> 并无区别。它们都只是 List,而 JVM 并不知道其中具体放的是什么类型。
来看一个示例:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!
这里的门道在于:我们创建了两个列表——一个用于字符串,另一个用于数字。在编译层面,Java 会严格保证你不会往 List<String> 里放入除字符串以外的东西,也不会往 List<Integer> 里放入非数字的东西。但一旦程序运行起来,这些区别就消失了。对 JVM 而言,这两个对象都只是 ArrayList,它已经无法检查这些列表“应该”存放什么类型的元素。因此,对两个列表的类进行比较(stringList.getClass() == intList.getClass())会返回 true。
由此得到一个重要结论:Java 中的泛型主要用于编译期的便利与类型安全。但在运行时,这些“标签”会消失。因此,如果你序列化一个集合,写入文件的只有数据本身,而不会包含关于泛型类型的信息。也就是说会保存一个值列表,但光凭文件无法判断它原本是 List<String>,还是 List<Object> 或 List<Integer>。
再举一例:序列化与反序列化 List<String>
import java.io.*;
import java.util.*;
public class GenericSerializationDemo {
public static void main(String[] args) throws Exception {
List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
oos.writeObject(fruits);
}
// 反序列化
List<String> loadedFruits;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
loadedFruits = (List<String>) ois.readObject();
}
System.out.println(loadedFruits); // [苹果, 香蕉, 橙子]
}
}
请注意这一行:
loadedFruits = (List<String>) ois.readObject();
这里我们显式将结果强制转换为 List<String>,但在运行时它其实只是一个 ArrayList。编译器无法验证它是否真的是字符串列表,如果其中突然出现了非字符串的元素——我们就会在程序运行时得到 ClassCastException。
2. 反序列化泛型集合时的问题
元素类型信息的丢失
由于泛型参数信息被擦除,反序列化之后,Java 无法保证集合中的对象就是你所期望的类型。你得到的只是一个“原始”集合(raw type),编译器不会抱怨,但问题可能会在运行时暴露。
问题演示
List rawList = new ArrayList();
rawList.add("猫");
rawList.add(42); // Integer!
// 反序列化
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!
Unchecked cast warning
编译器会如实给出潜在问题的警告:
Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
这个警告表示你在未经检查的情况下进行了类型转换,集合中可能包含意料之外类型的对象。
3. 泛型集合序列化的特点
文件中不包含泛型参数信息
当你序列化 List<String> 和 List<Integer> 时,文件中不会有任何关于它是字符串还是数字的说明。集合的内容是“如实”序列化的——对象按顺序写出。
如果你用文本编辑器打开序列化文件,也不会看到任何关于 <String> 或 <Integer> 的字样。这些信息只存在于源代码和编译器层面。
示例:序列化不同的集合
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("一", "二", "三");
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
oos.writeObject(numbers);
oos.writeObject(words);
}
文件 test.ser 中就是两个 ArrayList 对象,不包含任何泛型参数信息。
反序列化“原始”集合的问题
如果你以原始类型(raw type)序列化了列表,却按 List<String> 去反序列化,编译器无法验证类型正确性,运行时就可能出错。
4. 序列化泛型集合的最佳实践
注明期望的元素类型。
如果你的 API 会序列化集合,请务必说明期望的元素类型。例如:“该方法返回序列化的 List<User>”。
在反序列化后检查元素类型。
反序列化集合后,最好检查所有元素是否为期望类型(尤其当数据来源不受你控制时)。
for (Object obj : loadedList) {
if (!(obj instanceof String)) {
throw new IllegalStateException("预期为字符串,但发现:" + obj.getClass());
}
}
使用不可变集合。
如果你只需要以只读方式序列化集合,请使用不可变集合——List.copyOf、Collections.unmodifiableList。这有助于避免反序列化后数据被意外修改。
不要在同一个集合中混合多种类型。
尽量不要序列化元素类型混杂的集合(例如内部包含不同类的 List<Object>)。这会增加反序列化难度并可能导致错误。
谨慎使用警告抑制。
如果你确信自己反序列化的是具有正确元素类型的集合,可以使用注解 @SuppressWarnings("unchecked") 来抑制编译器警告:
@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();
但请有意识地使用——很容易把问题一直隐藏到生产环境。
5. 示例:包含自定义类的集合的序列化与反序列化
假设我们有一个 User 类:
import java.io.Serializable;
public class User implements Serializable {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return name + " (" + age + ")";
}
}
序列化用户列表:
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25)
);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
oos.writeObject(users);
}
// 反序列化
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
loadedUsers = (List<User>) ois.readObject();
}
System.out.println(loadedUsers); // [Alice (30), Bob (25)]
一切正常! 但如果有人在序列化文件中塞入了其他类的对象,当你尝试把元素当作 User 来读取时,可能会得到 ClassCastException。
6. 嵌套泛型集合的序列化
集合可以是嵌套的,例如:List<List<String>>、Map<String, List<User>> 等。Java 会递归地序列化这类结构,但规则不变:
- 所有嵌套的集合与元素都必须是可序列化的。
- 泛型参数信息仍然会被擦除。
示例:序列化列表的列表
List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
oos.writeObject(matrix);
}
// 反序列化
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
loadedMatrix = (List<List<String>>) ois.readObject();
}
System.out.println(loadedMatrix); // [[a, b], [c, d]]
7. 一些有用的细节
不同实现类型的泛型集合的序列化
有时你以一种集合实现进行序列化,却按另一种实现进行反序列化。例如,序列化的是 ArrayList,反序列化时却按 LinkedList 来转换。这会导致类型转换错误:
List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!
建议:总是以序列化时的相同类型来反序列化,或者在不关心具体实现时使用接口(List)。
使用库(例如,Gson、Jackson)
用于 JSON 的库(例如 Gson、Jackson)能够序列化/反序列化带泛型的集合,但由于类型擦除,在反序列化时需要显式提供类型。以 Gson 为例:
Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
8. Map 和 Set 中的泛型与序列化
以上规则同样适用于其他带泛型的集合:
- 序列化 Map<String, Integer> 时,键和值的类型信息不会被保存。
- 反序列化时需要进行相应的类型转换,并对内容保持警惕。
示例:序列化 Map
Map<String, Integer> scores = new HashMap<>();
scores.put("Basil", 90);
scores.put("Peter", 85);
// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
oos.writeObject(scores);
}
// 反序列化
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
loadedScores = (Map<String, Integer>) ois.readObject();
}
System.out.println(loadedScores); // {Basil=90, Peter=85}
9. 序列化泛型集合的常见错误
错误 1:反序列化时的 ClassCastException。 如果你将集合反序列化为 List<String>,而其中出现了其他类型的对象,运行时就会得到 ClassCastException。务必检查集合内容!
错误 2:由于不可序列化的元素导致 NotSerializableException。 如果集合中哪怕只有一个元素没有实现 Serializable,序列化就会以 NotSerializableException 失败。请检查所有可能出现在集合中的类是否可序列化。
错误 3:泛型参数信息的丢失。 反序列化后不要依赖泛型参数——运行时并不存在它们。如对数据的正确性有疑虑,请进行显式的类型检查。
错误 4:集合实现类型不匹配。 序列化的是 ArrayList,却按 LinkedList 进行反序列化——会导致转换错误。尽量用与序列化时相同的类型进行反序列化。
错误 5:类版本不兼容。 如果集合元素的类结构在序列化之后发生了变化(例如新增了字段),反序列化时可能出错。使用 serialVersionUID 来进行版本控制。
GO TO FULL VERSION