1. Introducción
La serialización automática es como el piloto automático de un avión: funciona de maravilla mientras todo sigue el plan. Pero cuando aparecen condiciones especiales, queda claro que un mecanismo simple ya no basta. Imagina que necesitas guardar un objeto, pero no todos sus campos: algunos datos son temporales y otros demasiado sensibles como para escribirlos en un archivo. O al contrario — al guardar necesitas añadir algo propio: por ejemplo, una versión o una suma de comprobación. También puede ocurrir que antes de escribir o cargar los datos debas realizar una validación o una transformación. Y a veces la tarea es aún más compleja: garantizar la compatibilidad con versiones anteriores de la clase si su estructura ha cambiado con el tiempo.
En estas situaciones se hace evidente: la serialización estándar por sí sola no basta. Hay que tomar el control.
Métodos especiales de serialización: writeObject y readObject
En Java hay dos métodos especiales que permiten controlar por completo el proceso de serialización y deserialización de un objeto:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
¡Importante!
- Los métodos deben ser precisamente private (no public, no protected, no package-private).
- Las firmas deben coincidir con las mostradas arriba.
- Si estos métodos están declarados en tu clase, se llamarán en lugar de la serialización/deserialización estándar.
¿Cómo funciona?
Cuando llamas a ObjectOutputStream.writeObject(obj), la JVM primero busca en la clase de obj el método private void writeObject(ObjectOutputStream). Si existe — se invoca ese. De manera análoga, durante la deserialización se llama a private void readObject(ObjectInputStream).
Si los métodos no están declarados, se usa la serialización estándar.
Cómo funcionan writeObject y readObject
Firmas de los métodos
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Dentro de estos métodos debes llamar a:
- out.defaultWriteObject(); — para serializar los campos estándar (no transient) de la superclase y de la clase actual.
- in.defaultReadObject(); — para deserializar los campos estándar.
Si no llamas a estos métodos, los campos estándar no se serializarán y, al deserializar, el objeto quedará «vacío». Es como olvidar meter el pasaporte en la maleta: formalmente has llegado, pero no podrás demostrar quién eres.
2. Ejemplo: añadimos una suma de comprobación en la serialización
Veamos un ejemplo práctico. Supongamos que tenemos una clase de usuario y queremos añadir al objeto una suma de comprobación durante la serialización para comprobar la integridad de los datos en la deserialización.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// campo transient: no se serializa
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Serialización personalizada
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Guardamos los campos estándar
int sum = calculateChecksum();
out.writeInt(sum); // Escribimos la suma de comprobación
System.out.println("[LOG] Serialización de User: checksum=" + sum);
}
// Deserialización personalizada
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Restauramos los campos estándar
int sum = in.readInt(); // Leemos la suma de comprobación
int actual = calculateChecksum();
System.out.println("[LOG] Deserialización de User: checksum=" + sum + ", real=" + actual);
if (sum != actual) {
throw new IOException("¡Los datos están dañados! La suma de comprobación no coincide.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Ejemplo de uso:
// Guardamos el objeto
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Cargamos el objeto
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Objeto restaurado: " + loaded);
}
¿Qué ocurre?
- Durante la serialización se llama a writeObject, se guardan los campos estándar + la suma de comprobación.
- Durante la deserialización se llama a readObject, se restauran los campos + se comprueba la suma de comprobación.
- Verás un registro en la consola y, si algo va mal, se lanzará una excepción.
3. Excluir datos sensibles de la serialización
A veces necesitas que ciertos campos no se serialicen (por ejemplo, contraseñas). Para ello puedes usar la palabra clave transient (más detalles en la próxima lección), pero también puedes no serializar manualmente el campo si implementas writeObject.
Ejemplo:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient: no se serializa
// Pero también se puede hacer así:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// ¡No escribimos password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password permanece como null
}
}
Atención:
Si quieres serializar solo una parte del objeto — simplemente no escribas los campos innecesarios en el flujo.
4. Llamar a los métodos de la superclase: defaultWriteObject y defaultReadObject
Dentro de tus métodos writeObject y readObject casi siempre debes llamar a defaultWriteObject() y defaultReadObject(). Es como pulsar «guardar borrador» antes de añadir tus propias notas.
Estos métodos se encargan de la serialización estándar de todos los campos no transient ni static de la clase actual y de la superclase. Si no los llamas, esos campos no se serializarán y quedarán vacíos al deserializar.
Ejemplo de comportamiento incorrecto:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // olvidamos llamarlo!
out.writeInt(123); // algo propio
}
En este caso, los campos estándar simplemente no se guardarán.
5. Práctica: registro del proceso de serialización
Añadamos registro a nuestra clase de usuario para ver cuándo se producen la serialización y la deserialización.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Serialización de Person: " + name + ", edad " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Deserialización de Person: " + name + ", edad " + age);
}
}
Uso:
Person p = new Person("Bob", 30);
// Guardamos en un archivo
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Cargamos desde un archivo
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Resultado:
En la consola verás mensajes indicando que el objeto se serializa y se deserializa.
6. Errores típicos al usar writeObject/readObject
Error n.º 1: No se llamó a defaultWriteObject/defaultReadObject. Si olvidas llamar a estos métodos, los campos estándar no se serializarán y el objeto, tras la deserialización, quedará vacío o incorrecto.
Error n.º 2: Firma incorrecta de los métodos. Los métodos deben ser estrictamente private void writeObject(ObjectOutputStream) y private void readObject(ObjectInputStream). Si los haces public/protected o cambias los parámetros, no se invocarán automáticamente.
Error n.º 3: Excepción en el método. Si se produce una excepción en writeObject o readObject, la serialización o la deserialización se interrumpirá y el objeto no se guardará/cargará correctamente.
Error n.º 4: Olvidar la serialización/deserialización de la superclase. Si tu clase hereda de otra clase serializable, asegúrate de llamar a defaultWriteObject/defaultReadObject; de lo contrario, los campos de la superclase no se guardarán.
Error n.º 5: Serialización de datos sensibles. Si olvidaste excluir contraseñas u otros datos privados, terminarán en el archivo serializado. Usa transient o no los serialices manualmente.
GO TO FULL VERSION