CodeGym /Cursos /JAVA 25 SELF /Introdução à serialização de objetos: para que serve

Introdução à serialização de objetos: para que serve

JAVA 25 SELF
Nível 42 , Lição 0
Disponível

1. Por que precisamos de serialização

Imagine que seu objeto — são as coisas que você leva nas férias. A serialização é a embalagem de todo o conteúdo da mala em um contêiner especial que pode ser colocado na bagagem ou enviado pelo correio. A desserialização, por sua vez, é a abertura desse contêiner e a obtenção das coisas em seu estado original.

Em essência, a serialização transforma um objeto em um fluxo de bytes que pode ser salvo em um arquivo, enviado pela rede ou simplesmente mantido na memória. A desserialização faz o inverso: restaura o objeto a partir desse fluxo. Simplificando ao máximo, serializar é como “congelar” um objeto para depois “descongelá-lo” e obtê-lo de volta no mesmo estado.

Salvar o estado dos objetos entre as execuções do programa

Um dos cenários mais comuns é a persistência do estado do programa. Por exemplo, você tem uma lista de usuários, resultados de um jogo ou configurações de aplicativo. Tudo isso é conveniente de armazenar diretamente como objetos. Para que os dados não se percam entre execuções, eles são serializados em um arquivo e, na próxima inicialização, desserializados.

Um bom exemplo é um save de jogo comum. Quando o jogador passa de fase, seu progresso é “congelado” e gravado em um arquivo por meio da serialização. No dia seguinte, ele abre o jogo e o progresso é “descongelado”: os dados do arquivo se transformam novamente em objetos, e o jogador continua do ponto em que parou.

Vamos criar um save simples assim:

import java.io.*;

// A classe do jogador deve ser Serializable
class Player implements Serializable {
    String name;
    int score;

    Player(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public class GameSaveExample {
    public static void main(String[] args) throws Exception {
        // Criamos um objeto Player
        Player player = new Player("Ihor", 1500);

        // --- Salvando (serialização) ---
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
            out.writeObject(player);
            System.out.println("Progresso salvo!");
        }

        // --- Carregando (desserialização) ---
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
            Player loaded = (Player) in.readObject();
            System.out.println("Progresso carregado: " + loaded.name + " com pontos " + loaded.score);
        }
    }
}

Atenção: para que esse código funcione, a classe Player deve implementar a interface Serializable. Mais detalhes sobre ela — na próxima aula!

  • Player é uma classe comum com os campos nome e pontos, marcada pela interface Serializable (implements Serializable).
  • ObjectOutputStream grava o objeto no arquivo "save.dat".
  • ObjectInputStream lê esse mesmo objeto de volta.
  • Como resultado, temos um save de verdade: na próxima execução o programa carregará o objeto do jogador com o mesmo estado.

Transmissão de objetos pela rede e entre JVMs

Em sistemas distribuídos, frequentemente é necessário transmitir objetos entre diferentes programas ou até máquinas diferentes. Por exemplo, você tem um cliente e um servidor que precisam trocar mensagens. A serialização permite “empacotar” um objeto de um lado, enviá-lo pela rede e “desempacotá-lo” do outro lado.

Exemplo: o cliente envia ao servidor um objeto de pedido (Order), o servidor o recebe, desserializa e processa.

Uso em tecnologias Java

  • RMI (Remote Method Invocation): permite invocar métodos de objetos remotos — a serialização é necessária para transferir argumentos e valores de retorno.
  • Sessões HTTP: em servlets, objetos na sessão são serializados ao reiniciar o contêiner.
  • JMS (Java Message Service): mensagens entre componentes podem ser serializadas.
  • Armazenamento em cache: objetos podem ser serializados para armazenamento em cache (em disco ou em armazenamento distribuído).

Cache e portabilidade

Se você deseja salvar rapidamente resultados intermediários (por exemplo, para cache), a serialização é uma ótima ferramenta. Você serializa o objeto, o salva em disco ou na memória e, depois, o restaura rapidamente sem recomputações.

2. Exemplos de cenários de uso da serialização

Salvar uma coleção de usuários em arquivo

Suponha que você tenha a classe User:

public class User {
    String name;
    int age;
    // ... outros campos
}

E você tem uma lista de usuários:

List<User> users = new ArrayList<>();
users.add(new User("Vasya", 25));
users.add(new User("Masha", 30));
// ... e assim por diante

Para salvar essa lista em um arquivo, você a serializa. Quando necessário — você desserializa e obtém a mesma lista com os mesmos usuários. Lembre-se de que a classe User (e todos os seus campos) deve dar suporte à serialização, isto é, implementar Serializable.

Enviar uma mensagem entre cliente e servidor

Um exemplo clássico é um chat. O usuário escreve uma mensagem, o objeto Message é serializado e enviado pela rede. O servidor recebe o fluxo de bytes, desserializa o objeto, o processa e, possivelmente, o encaminha.

import java.io.*;
import java.net.*;

// A mensagem deve ser Serializable
class Message implements Serializable {
    String text;

    Message(String text) {
        this.text = text;
    }
}

// Servidor
class Server {
    public static void main(String[] args) throws Exception {
        try (ServerSocket serverSocket = new ServerSocket(5000)) {
            System.out.println("O servidor está aguardando conexão...");
            Socket socket = serverSocket.accept();
            System.out.println("Cliente conectado!");

            try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
                Message msg = (Message) in.readObject();
                System.out.println("Mensagem recebida: " + msg.text);
            }
        }
    }
}

// Cliente
class Client {
    public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket("localhost", 5000)) {
            try (ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream())) {
                Message msg = new Message("Olá, servidor!");
                out.writeObject(msg);
                System.out.println("Mensagem enviada!");
            }
        }
    }
}

Como funciona:

  1. Primeiro, execute o Server (ele aguarda a conexão).
  2. Depois, execute o Client (ele se conecta a "localhost:5000").
  3. O cliente serializa o objeto Message e o envia pelo socket.
  4. O servidor recebe o fluxo de bytes, desserializa e imprime o texto.

Aqui usamos sockets (ServerSocket, Socket) — é um mecanismo de comunicação em rede que você estudará depois. O importante agora não são os detalhes da rede, mas a própria ideia: o cliente cria um objeto Message, o serializa e envia; o servidor recebe o fluxo de bytes, desserializa de volta para um objeto e imprime a mensagem. Assim, mesmo que ainda não esteja claro o que são as classes ServerSocket e Socket, o exemplo mostra o valor da serialização: graças a ela, é possível “empacotar” um objeto, enviá-lo pela rede e, do outro lado, desempacotá-lo sem conversões extras.

Cache de objetos

Em aplicativos grandes, o cache é frequentemente usado para acelerar o funcionamento. Por exemplo, resultados de cálculos complexos são serializados e salvos no cache (arquivo, banco de dados, armazenamento distribuído). Na próxima solicitação, o resultado pode ser rapidamente restaurado desserializando o objeto.

import java.io.*;

// Resultado de cálculos que queremos armazenar em cache
class Result implements Serializable {
    int value;

    Result(int value) {
        this.value = value;
    }
}

public class CacheExample {
    private static final String CACHE_FILE = "cache.dat";

    public static void main(String[] args) throws Exception {
        Result result;

        // Verificamos se existe cache
        File file = new File(CACHE_FILE);
        if (file.exists()) {
            // Carregamos o resultado do cache
            try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
                result = (Result) in.readObject();
                System.out.println("Carregado do cache: " + result.value);
            }
        } else {
            // Cálculo "pesado" (no exemplo, apenas o quadrado do número)
            int x = 12345;
            System.out.println("Calculando... (isso é demorado)");
            result = new Result(x * x);

            // Salvamos o resultado no cache
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
                out.writeObject(result);
                System.out.println("Salvo no cache: " + result.value);
            }
        }
    }
}

3. Limitações e riscos da serialização

A serialização é uma ferramenta poderosa, mas não isenta de armadilhas. Vamos discutir as principais limitações e riscos.

Nem todos os objetos podem ser serializados

Em Java, nem todos os objetos podem ser serializados “prontos para uso”. Por exemplo, objetos vinculados a recursos externos (arquivos, conexões de rede, streams de entrada/saída) não são serializáveis. Isso é lógico: não é possível serializar um “arquivo aberto” ou uma conexão de rede ativa — seu estado depende do sistema operacional e do ambiente de execução.

Exemplo: uma classe com um campo do tipo FileInputStream não pode ser serializada — ao tentar serializar ocorrerá um erro.

Questões de segurança

A serialização é uma possível brecha de segurança. Se você desserializa dados obtidos de uma fonte não confiável (por exemplo, da internet), um invasor pode enviar um fluxo de bytes malicioso que levará a um comportamento inesperado do seu programa e, às vezes, até à execução de código malicioso.

Regra: Nunca desserialize dados de fontes não confiáveis! É como aceitar um pacote de um remetente desconhecido — pode haver qualquer coisa dentro.

Compatibilidade de versões

Se você alterou a estrutura da classe (por exemplo, adicionou ou removeu um campo), objetos serializados anteriormente podem se tornar incompatíveis com a nova versão da classe. Isso pode levar a erros na desserialização. Esse assunto será abordado em mais detalhes nas próximas aulas.

Desempenho

A serialização binária em Java é bastante rápida, mas às vezes não é a mais compacta e nem sempre é conveniente para troca com outras linguagens de programação. Para integração com sistemas externos, formatos de texto (JSON, XML) são frequentemente usados.

4. Erros comuns no primeiro contato com a serialização

Erro nº 1: tentar serializar um objeto que não implementa a interface Serializable.
Como resultado, você receberá a exceção NotSerializableException. Não se esqueça de declarar explicitamente implements Serializable na classe e garantir que todos os campos também sejam serializáveis!

Erro nº 2: serializar objetos com campos não serializáveis.
Se sua classe contém um campo de um tipo que não suporta serialização (por exemplo, um stream ou uma conexão com o BD), a serialização não funcionará. A solução é marcar esses campos como transient (mais sobre isso depois).

Erro nº 3: desserializar dados de fontes não confiáveis.
Isso pode levar a vulnerabilidades de segurança ou até à execução de código malicioso. Confie apenas em dados que foram serializados pelo seu próprio programa!

Erro nº 4: alterações na estrutura da classe após a serialização.
Se você salvou um objeto e depois adicionou ou removeu um campo na classe, ao tentar desserializar surgirá um erro ou valores “estranhos”. Mais detalhes — nas próximas aulas.

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