CodeGym /Cursos /JAVA 25 SELF /Introducción a la serialización de objetos: para qué sirv...

Introducción a la serialización de objetos: para qué sirve

JAVA 25 SELF
Nivel 42 , Lección 0
Disponible

1. Para qué sirve la serialización

Imagina que tu objeto son las cosas que te llevas de vacaciones. La serialización es empaquetar todo el contenido de la maleta en un contenedor especial que puedes poner en el equipaje o enviar por correo. La deserialización, por su parte, es desempaquetar ese contenedor y recuperar las cosas en su forma original.

En esencia, la serialización convierte un objeto en un flujo de bytes que puede guardarse en un archivo, transmitirse por la red o simplemente mantenerse en memoria. La deserialización hace lo contrario: reconstruye el objeto a partir de ese flujo. Simplificando, la serialización es como «congelar» un objeto para luego «descongelarlo» y recuperarlo en el mismo estado.

Guardar el estado de los objetos entre ejecuciones del programa

Uno de los escenarios más habituales es el guardado del estado de la aplicación. Por ejemplo, tienes una lista de usuarios, el progreso de un juego o la configuración de la app. Todo esto es cómodo mantenerlo directamente en forma de objetos. Para que los datos no se pierdan entre ejecuciones, se serializan a un archivo y, en el siguiente inicio, se deserializan.

Un buen ejemplo es el guardado en un juego. Cuando el jugador supera un nivel, su progreso se «congela» y se escribe en un archivo mediante serialización. Al día siguiente inicia el juego y el progreso se «descongela»: los datos del archivo se convierten de nuevo en objetos y el jugador continúa desde donde lo dejó.

Vamos a crear un guardado simple:

import java.io.*;

// La clase del jugador debe 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 {
        // Creamos un objeto Player
        Player player = new Player("Ihor", 1500);

        // --- Guardado (serialización) ---
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("save.dat"))) {
            out.writeObject(player);
            System.out.println("Progreso guardado.");
        }

        // --- Carga (deserialización) ---
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("save.dat"))) {
            Player loaded = (Player) in.readObject();
            System.out.println("Progreso cargado: " + loaded.name + " con puntos " + loaded.score);
        }
    }
}

Ten en cuenta: para que este código funcione, la clase Player debe implementar la interfaz Serializable. ¡Más detalles sobre ella en la próxima lección!

  • Player es una clase normal con los campos nombre y puntos, marcada con la interfaz Serializable (implements Serializable).
  • ObjectOutputStream escribe el objeto en el archivo "save.dat".
  • ObjectInputStream lee ese mismo objeto de vuelta.
  • Como resultado, obtenemos un guardado real: en la siguiente ejecución, el programa cargará el objeto del jugador con el mismo estado.

Transferencia de objetos por la red y entre JVM

En sistemas distribuidos a menudo se requiere transferir objetos entre distintos programas o incluso máquinas. Por ejemplo, tienes un cliente y un servidor que deben intercambiar mensajes. La serialización permite «empaquetar» un objeto en un lado, enviarlo por la red y «desempaquetarlo» en el otro.

Ejemplo: Un cliente envía al servidor un objeto de pedido (Order); el servidor lo recibe, lo deserializa y lo procesa.

Uso en tecnologías de Java

  • RMI (Remote Method Invocation): permite invocar métodos de objetos remotos; la serialización es necesaria para transferir argumentos y valores de retorno.
  • Sesiones HTTP: en servlets, los objetos de sesión se serializan al reiniciar el contenedor.
  • JMS (Java Message Service): los mensajes entre componentes pueden ser serializados.
  • Caché: los objetos pueden serializarse para almacenarlos en caché (en disco o en un almacén distribuido).

Caché y portabilidad

Si quieres guardar rápidamente resultados intermedios (por ejemplo, para cachear), la serialización es una gran herramienta. Serializas el objeto, lo guardas en disco o en memoria y luego lo recuperas rápidamente sin recomputar.

2. Ejemplos de escenarios de uso de la serialización

Guardar una colección de usuarios en un archivo

Supongamos que tienes una clase User:

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

Y tienes una lista de usuarios:

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

Para guardar esta lista en un archivo, la serializas. Cuando lo necesites, la deserializas y obtienes la misma lista con los mismos usuarios. Recuerda que la clase User (y todos sus campos) debe soportar la serialización, es decir, implementar Serializable.

Enviar un mensaje entre cliente y servidor

El ejemplo clásico es un chat. El usuario escribe un mensaje, el objeto Message se serializa y se envía por la red. El servidor recibe el flujo de bytes, deserializa el objeto, lo procesa y, posiblemente, lo reenvía.

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

// El mensaje debe 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("El servidor espera una conexión...");
            Socket socket = serverSocket.accept();
            System.out.println("¡El cliente se ha conectado!");

            try (ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) {
                Message msg = (Message) in.readObject();
                System.out.println("Mensaje recibido: " + 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("¡Hola, servidor!");
                out.writeObject(msg);
                System.out.println("Mensaje enviado.");
            }
        }
    }
}

Cómo funciona:

  1. Primero se ejecuta Server (queda a la espera de una conexión).
  2. Luego se ejecuta Client (se conecta a "localhost:5000").
  3. El cliente serializa el objeto Message y lo envía a través del socket.
  4. El servidor recibe el flujo de bytes, lo deserializa e imprime el texto.

Aquí usamos sockets (ServerSocket, Socket) — un mecanismo de comunicación en red que estudiarás más adelante. Ahora lo importante no son los detalles de la red, sino la idea: el cliente crea un objeto Message, lo serializa y lo envía; el servidor recibe el flujo de bytes, lo deserializa de nuevo a un objeto e imprime el mensaje. Así, incluso si aún no está claro qué son las clases ServerSocket y Socket, el ejemplo muestra el valor de la serialización: gracias a ella puedes «empaquetar» un objeto, enviarlo por la red y, en el otro extremo, desempaquetarlo sin conversiones adicionales.

Cachear objetos

En aplicaciones grandes se usa a menudo el caché para acelerar el funcionamiento. Por ejemplo, los resultados de cálculos costosos se serializan y se guardan en el caché (archivo, base de datos, almacén distribuido). En la siguiente petición el resultado puede recuperarse rápidamente deserializando el objeto.

import java.io.*;

// Resultado de los cálculos que queremos cachear
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;

        // Comprobamos si existe la caché
        File file = new File(CACHE_FILE);
        if (file.exists()) {
            // Cargamos el resultado desde la caché
            try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
                result = (Result) in.readObject();
                System.out.println("Cargado desde la caché: " + result.value);
            }
        } else {
            // Cálculo "pesado" (para el ejemplo, simplemente el cuadrado de un número)
            int x = 12345;
            System.out.println("Calculando... (esto lleva tiempo)");
            result = new Result(x * x);

            // Guardamos el resultado en la caché
            try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
                out.writeObject(result);
                System.out.println("Guardado en la caché: " + result.value);
            }
        }
    }
}

3. Limitaciones y riesgos de la serialización

La serialización es una herramienta potente, pero no está exenta de trampas. Veamos las principales limitaciones y riesgos.

No todos los objetos se pueden serializar

En Java no todos los objetos se pueden serializar «de fábrica». Por ejemplo, los objetos que están vinculados a recursos externos (archivos, conexiones de red, flujos de E/S) no son serializables. Es lógico: no es posible serializar un «archivo abierto» o una conexión de red «viva», ya que su estado depende del sistema operativo y del entorno de ejecución.

Ejemplo: Una clase con un campo de tipo FileInputStream no se puede serializar; al intentar serializarla se producirá un error.

Seguridad

La serialización es una posible puerta a problemas de seguridad. Si deserializas datos recibidos de una fuente no fiable (por ejemplo, de internet), un atacante puede inyectar un flujo de bytes malicioso que provoque un comportamiento inesperado de tu programa e incluso la ejecución de código malicioso.

Regla: ¡Nunca deserialices datos de fuentes no confiables! Es como aceptar un paquete de un remitente desconocido: dentro puede haber cualquier cosa.

Compatibilidad entre versiones

Si cambias la estructura de la clase (por ejemplo, añades o eliminas un campo), los objetos serializados previamente pueden resultar incompatibles con la nueva versión de la clase. Esto puede provocar errores durante la deserialización. Este tema se tratará con más detalle en próximas lecciones.

Rendimiento

La serialización binaria en Java es bastante rápida, pero a veces no es la más compacta y no siempre es cómoda para el intercambio con otros lenguajes de programación. Para el intercambio con sistemas externos se suelen usar formatos de texto (JSON, XML).

4. Errores típicos al empezar con la serialización

Error n.º 1: intentar serializar un objeto que no implementa la interfaz Serializable.
Como resultado obtendrás la excepción NotSerializableException. ¡No olvides indicar explícitamente implements Serializable en la clase y asegurarte de que todos los campos también sean serializables!

Error n.º 2: serializar objetos con campos no serializables.
Si tu clase contiene un campo de un tipo que no soporta la serialización (por ejemplo, un flujo o una conexión a la BD), la serialización no funcionará. La solución es marcar dichos campos como transient (más adelante hablaremos de ello).

Error n.º 3: deserializar datos de fuentes no confiables.
Esto puede llevar a vulnerabilidades de seguridad o incluso a la ejecución de código malicioso. ¡Confía solo en los datos que hayan sido serializados por tu propia aplicación!

Error n.º 4: cambios en la estructura de la clase después de serializar.
Si guardaste un objeto y luego añadiste o eliminaste un campo en la clase, al intentar deserializar aparecerá un error o valores «extraños». Más detalles en las próximas lecciones.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION