CodeGym /Cursos /JAVA 25 SELF /Serialização de coleções com generics: particularidades

Serialização de coleções com generics: particularidades

JAVA 25 SELF
Nível 45 , Lição 1
Disponível

1. Serialização e desserialização de coleções com generics

Generics (tipos genéricos) em Java não são mágica, mas sim uma ilusão sustentada pelo compilador. Na fase de compilação, a informação sobre os tipos dos parâmetros genéricos é apagada (isso se chama type erasure, ou “apagamento de tipos”). Ou seja, em tempo de execução (runtime) a coleção List<String> não se diferencia de List<Object> ou List<Integer>. Todas elas são apenas List, e a JVM não sabe quais tipos exatamente estão lá dentro.

Vamos considerar um exemplo:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

Aqui há um detalhe. Criamos duas listas — uma para strings e outra para números. No nível do compilador, o Java controla rigorosamente para que você não coloque em List<String> nada além de strings e, em List<Integer>, nada além de números. Mas, assim que o programa é executado, as diferenças somem. Para a JVM, ambos os objetos são apenas ArrayList, e ela já não consegue verificar quais elementos deveriam ser armazenados. É por isso que a comparação das classes das duas listas (stringList.getClass() == intList.getClass()) retorna true.

Daí uma conclusão importante: generics em Java servem principalmente para conveniência e segurança na fase de compilação. Mas, em runtime, esses “rótulos” se perdem. Portanto, se você serializa uma coleção, apenas os dados em si vão para o arquivo, e não a informação dos tipos genéricos. Ou seja, é preservada a lista de valores, mas pelo arquivo não é possível saber que era exatamente um List<String>, e não um List<Object> ou List<Integer>.

Mais um exemplo: serialização e desserialização de 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("Maçã");
        fruits.add("Banana");
        fruits.add("Laranja");

        // Serialização
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("fruits.ser"))) {
            oos.writeObject(fruits);
        }

        // Desserialização
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [Maçã, Banana, Laranja]
    }
}

Atenção à linha:

loadedFruits = (List<String>) ois.readObject();

Aqui fazemos um cast explícito para o tipo List<String>, embora, em runtime, isso seja apenas um ArrayList. O compilador não consegue verificar se é realmente uma lista de strings e, se por acaso houverem elementos que não sejam strings — teremos um ClassCastException em tempo de execução.

2. Problemas na desserialização de coleções com generics

Perda de informação sobre o tipo dos elementos

Como a informação sobre os parâmetros genéricos é apagada, após a desserialização o Java não pode garantir que a coleção contenha exatamente os objetos que você espera. Tudo o que você obtém é uma coleção “crua” (raw type), e o compilador não reclama, mas o problema pode aparecer em runtime.

Demonstração do problema

List rawList = new ArrayList();
rawList.add("Gato");
rawList.add(42); // Integer!
// Desserialização
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Aviso de conversão unchecked

O compilador vai avisar sobre o problema potencial:

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Esse aviso indica que você está fazendo um cast sem verificação, e a coleção pode conter objetos de um tipo inesperado.

3. Particularidades da serialização de coleções com generics

Não há informação sobre parâmetros genéricos no arquivo

Quando você serializa List<String> e List<Integer>, o arquivo não contém qualquer informação de que eram strings ou números. O conteúdo da coleção é serializado “como está” — os objetos em ordem.

Se você abrir o arquivo serializado em um editor de texto, não verá menção a <String> nem a <Integer>. Tudo isso existe apenas no nível do código-fonte e do compilador.

Exemplo: serialização de coleções diferentes

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("um", "dois", "três");

// Serialização
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

No arquivo test.ser há apenas dois objetos do tipo ArrayList, sem qualquer informação sobre parâmetros genéricos.

Problema na desserialização de coleções “cruas”

Se você serializou uma lista sem parâmetro genérico (raw type) e a desserializa como List<String>, o compilador não consegue verificar a correção dos tipos, e erros em tempo de execução são possíveis.

4. Boas práticas para serialização de coleções com generics

Documente os tipos de elementos esperados.
Se sua API serializa uma coleção, deixe claro qual tipo de elemento é esperado. Por exemplo: “Este método retorna um List<User> serializado”.

Verifique os tipos dos elementos após a desserialização.
Depois de desserializar a coleção, é útil verificar se todos os elementos têm o tipo esperado (especialmente se a fonte dos dados não está sob seu controle).

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("Esperava uma String, mas foi encontrado: " + obj.getClass());
    }
}

Use coleções imutáveis.
Se você serializa uma coleção somente para leitura, use coleções imutáveis — List.copyOf, Collections.unmodifiableList. Isso ajuda a evitar alterações acidentais nos dados após a desserialização.

Não misture tipos em uma mesma coleção.
Evite serializar coleções com elementos de tipos diferentes (por exemplo, List<Object> com várias classes dentro). Isso complica a desserialização e pode levar a erros.

Use o supressão de avisos com cuidado.
Se você tem certeza de que está desserializando uma coleção com o tipo correto de elementos, é possível suprimir o aviso do compilador com a anotação @SuppressWarnings("unchecked"):

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

Mas use isso conscientemente — é fácil esconder o problema até chegar à produção.

5. Exemplo: serialização e desserialização de uma coleção com classe própria

Suponha que temos a classe 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 + ")";
    }
}

Serializamos a lista de usuários:

List<User> users = Arrays.asList(
    new User("Alice", 30),
    new User("Bob", 25)
);

// Serialização
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users.ser"))) {
    oos.writeObject(users);
}

// Desserialização
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)]

Tudo funciona! Mas, se alguém inserir no arquivo serializado um objeto de outra classe, você pode obter um ClassCastException ao tentar ler os elementos como User.

6. Serialização de coleções com generics aninhadas

Coleções podem ser aninhadas, por exemplo: List<List<String>>, Map<String, List<User>> etc. O Java serializa tais estruturas recursivamente, mas as regras permanecem as mesmas:

  • Todas as coleções aninhadas e seus elementos devem ser serializáveis.
  • A informação sobre os parâmetros genéricos continua sendo apagada.

Exemplo: serialização de uma lista de listas

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

// Serialização
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("matrix.ser"))) {
    oos.writeObject(matrix);
}

// Desserialização
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. Dicas úteis

Serialização de coleções com generics com implementações diferentes

Às vezes você serializa uma implementação de coleção e desserializa como outra. Por exemplo, serializou um ArrayList, mas desserializa como LinkedList. Isso levará a um erro de cast:

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

Dica: sempre desserialize para o mesmo tipo que foi serializado ou use a interface (List) se a implementação específica não for importante.

Uso de bibliotecas (por exemplo, Gson, Jackson)

Bibliotecas para JSON (por exemplo, Gson, Jackson) conseguem serializar/desserializar coleções com generics, mas exigem a indicação explícita do tipo na desserialização por causa do apagamento de tipos. Exemplo com Gson:

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Generics e serialização em Map e Set

Todas as regras acima valem também para outras coleções com generics:

  • Ao serializar Map<String, Integer>, a informação sobre os tipos de chaves e valores não é preservada.
  • Na desserialização, é preciso fazer cast para o tipo necessário e ficar atento ao conteúdo.

Exemplo: serialização de Map

Map<String, Integer> scores = new HashMap<>();
scores.put("Vasya", 90);
scores.put("Petya", 85);

// Serialização
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("scores.ser"))) {
    oos.writeObject(scores);
}

// Desserialização
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {Vasya=90, Petya=85}

9. Erros típicos na serialização de coleções com generics

Erro nº 1: ClassCastException na desserialização. Se você desserializa uma coleção como List<String>, mas houver um objeto de outro tipo dentro dela, você terá um ClassCastException em tempo de execução. Sempre verifique o conteúdo da coleção!

Erro nº 2: NotSerializableException por causa de um elemento não serializável. Se ao menos um elemento da coleção não implementar Serializable, a serialização terminará com NotSerializableException. Verifique a serializabilidade de todas as classes que possam estar na coleção.

Erro nº 3: Perda de informação sobre parâmetros genéricos. Após a desserialização, não confie nos parâmetros genéricos — eles não existem em runtime. Use verificações explícitas de tipos se houver dúvidas quanto à correção dos dados.

Erro nº 4: Incompatibilidade das implementações de coleções. Serializou um ArrayList, mas desserializa como LinkedList — você terá um erro de cast. Procure desserializar para o mesmo tipo que foi serializado.

Erro nº 5: Incompatibilidade de versões das classes. Se a estrutura da classe de um elemento da coleção mudou após a serialização (por exemplo, foi adicionado um campo), podem ocorrer erros na desserialização. Use serialVersionUID para controle de versões.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION