1. Serialization and deserialization of generic collections
Generics in Java are not magic but rather an illusion maintained by the compiler. At compile time, information about type parameters of generics is erased (this is called type erasure). That is, at runtime the collection List<String> is no different from List<Object> or List<Integer>. They are all just List, and the JVM does not know which exact types are stored there.
Let’s look at an example:
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
System.out.println(stringList.getClass() == intList.getClass()); // true!
There’s a catch here. We create two lists — one for strings, the other for numbers. At the compiler level, Java strictly ensures that you don’t put anything but strings into List<String> or anything but numbers into List<Integer>. But as soon as the program runs, the differences disappear. For the JVM, both objects are just ArrayList, and it can no longer verify which elements are supposed to be stored there. That’s why comparing the classes of the two lists (stringList.getClass() == intList.getClass()) returns true.
Hence an important conclusion: generics in Java are primarily for convenience and safety at compile time. But at runtime these “labels” are lost. Therefore, if you serialize a collection, only the data itself will be written to the file, not information about the generic types. In other words, the list of values is preserved, but from the file you cannot tell that it was exactly a List<String> rather than a List<Object> or List<Integer>.
Another example: serialization and deserialization of 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("Apple");
fruits.add("Banana");
fruits.add("Orange");
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
oos.writeObject(fruits);
}
// Deserialization
List<String> loadedFruits;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
loadedFruits = (List<String>) ois.readObject();
}
System.out.println(loadedFruits); // [Apple, Banana, Orange]
}
}
Pay attention to the line:
loadedFruits = (List<String>) ois.readObject();
Here we explicitly cast the result to List<String>, although at runtime it’s just an ArrayList. The compiler cannot verify that it’s actually a list of strings, and if non-strings appear there, you’ll get a ClassCastException while the program is running.
2. Problems when deserializing generic collections
Loss of element type information
Because information about generic parameters is erased, after deserialization Java cannot guarantee that the collection contains exactly the objects you expect. All you get is a raw collection (raw type): the compiler won’t complain, but the problem may surface at runtime.
Demonstration of the problem
List rawList = new ArrayList();
rawList.add("Cat");
rawList.add(42); // Integer!
// Deserialization
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!
Unchecked cast warning
The compiler will warn about a potential problem:
Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
This warning indicates that you are casting without verification, and the collection may contain objects of an unexpected type.
3. Specifics of serializing generic collections
No information about generic parameters in the file
When you serialize List<String> and List<Integer>, there will be no information in the file about whether they were strings or numbers. The contents of the collection are serialized “as is” — objects in order.
If you open the serialized file in a text editor, you won’t see anything about <String> or <Integer> there. All of that exists only at the source-code and compiler level.
Example: serializing different collections
List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("one", "two", "three");
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
oos.writeObject(numbers);
oos.writeObject(words);
}
In the file test.ser there are just two objects of type ArrayList, with no information about the generic parameters.
The problem with deserializing “raw” collections
If you serialized a list without a generic parameter (raw type) and then deserialize it as a List<String>, the compiler cannot verify type correctness, and runtime errors are possible.
4. Best practices for serializing generic collections
Document the expected element types.
If your API serializes a collection, be sure to state what element type is expected. For example: “This method returns a serialized List<User>”.
Validate element types after deserialization.
After deserializing a collection, it’s useful to verify that all elements have the expected type (especially if the data source is not under your control).
for (Object obj : loadedList) {
if (!(obj instanceof String)) {
throw new IllegalStateException("Expected a String, but found: " + obj.getClass());
}
}
Use immutable collections.
If you serialize a read-only collection, use immutable collections — List.copyOf, Collections.unmodifiableList. This helps avoid accidental modification of data after deserialization.
Do not mix types within a single collection.
Avoid serializing collections that contain elements of different types (for example, a List<Object> with various classes inside). This complicates deserialization and can lead to errors.
Use warning suppression carefully.
If you are sure you are deserializing a collection with the correct element type, you can suppress the compiler warning using the @SuppressWarnings("unchecked") annotation:
@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();
But use this deliberately — it’s easy to hide a problem until production.
5. Example: serializing and deserializing a collection with a custom class
Suppose we have a User class:
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 + ")";
}
}
Serialize a list of users:
List<User> users = Arrays.asList(
new User("Alice", 30),
new User("Bob", 25)
);
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
oos.writeObject(users);
}
// Deserialization
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)]
Everything works! But if someone slips an object of a different class into the serialized file, you may get a ClassCastException when you try to read elements as User.
6. Serialization of nested generic collections
Collections can be nested, for example: List<List<String>>, Map<String, List<User>>, and so on. Java serializes such structures recursively, but the rules remain the same:
- All nested collections and elements must be serializable.
- Information about generic parameters is still erased.
Example: serializing a list of lists
List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
oos.writeObject(matrix);
}
// Deserialization
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. Useful nuances
Serializing generic collections with different implementations
Sometimes you serialize one collection implementation and deserialize as another. For example, you serialized an ArrayList but deserialize as a LinkedList. This will lead to a type cast error:
List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!
Tip: Always deserialize to the same type you serialized, or use the interface (List) if you don’t care about the concrete implementation.
Using libraries (e.g., Gson, Jackson)
JSON libraries (for example, Gson, Jackson) can serialize/deserialize collections with generics but require an explicit type during deserialization because of type erasure. Example for Gson:
Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);
8. Generics and serialization in Map and Set
All the above rules also apply to other collections with generics:
- When serializing a Map<String, Integer>, information about key and value types is not preserved.
- When deserializing, you have to cast to the desired type and be careful about the contents.
Example: serializing a Map
Map<String, Integer> scores = new HashMap<>();
scores.put("John", 90);
scores.put("Peter", 85);
// Serialization
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
oos.writeObject(scores);
}
// Deserialization
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
loadedScores = (Map<String, Integer>) ois.readObject();
}
System.out.println(loadedScores); // {John=90, Peter=85}
9. Common mistakes when serializing generic collections
Error No. 1: ClassCastException during deserialization. If you deserialize a collection as a List<String> but it contains an object of a different type, you will get a ClassCastException at runtime. Always check the contents of the collection!
Error No. 2: NotSerializableException due to a non-serializable element. If at least one element of the collection does not implement Serializable, serialization will fail with NotSerializableException. Verify the serializability of all classes that may end up in the collection.
Error No. 3: Loss of information about generic parameters. After deserialization, do not rely on generic parameters — they do not exist at runtime. Use explicit type checks if you doubt the correctness of the data.
Error No. 4: Mismatched collection implementations. You serialized an ArrayList but deserialize as a LinkedList — you’ll get a cast error. Try to deserialize into the same type that was serialized.
Error No. 5: Class version incompatibility. If the structure of a class for an element of the collection changed after serialization (for example, a field was added), errors during deserialization are possible. Use serialVersionUID for version control.
GO TO FULL VERSION