1. Introducción
En Java, para la serialización de objetos se utiliza con mayor frecuencia la interfaz Serializable. Es simple: basta con implementar la interfaz y el objeto se puede escribir/leer con ObjectOutputStream/ObjectInputStream. Pero a veces esto no es suficiente:
- Se necesita controlar por completo qué campos y cómo se serializan.
- Se requiere garantizar la compatibilidad entre distintas versiones de la clase.
- Es importante reducir el tamaño del archivo serializado o acelerar el proceso.
Para estos casos en Java existe la interfaz Externalizable, una forma más «manual» y flexible de serialización.
En resumen:
- Serializable: serialización automática; Java decide qué y cómo escribir.
- Externalizable: serialización manual; se indica exactamente qué y cómo guardar/restaurar.
2. Contrato Externalizable: implementar writeExternal y readExternal
Para usar Externalizable, hay que:
- Implementar la interfaz java.io.Externalizable.
- Implementar obligatoriamente dos métodos:
- void writeExternal(ObjectOutput out) throws IOException
- void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
Ejemplo:
import java.io.*;
public class User implements Externalizable {
private String name;
private int age;
// ¡Constructor público sin parámetros obligatorio!
public User() {}
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Importante: el desarrollador decide qué campos se serializarán y en qué orden. Pero hay un requisito obligatorio: la clase debe tener un constructor public sin parámetros. Si no lo hay, durante la deserialización el programa lanzará InvalidClassException.
3. ¿Cuándo usar Externalizable?
Use Externalizable si:
- Se necesita control total sobre el formato de los datos. Por ejemplo, si quiere serializar solo parte de los campos o hacerlo en un orden/formato especial.
- Optimización de rendimiento y tamaño del archivo. La serialización estándar añade información de servicio (metadatos, nombres de clases, tipos, etc.). Con Externalizable se escriben solo los datos necesarios.
- Garantizar la compatibilidad retroactiva. Si cambia la estructura de la clase, se puede implementar manualmente la lógica de lectura de datos de versiones antiguas y nuevas.
- Serialización de objetos no estándar. Por ejemplo, si hay campos que no pueden serializarse de forma estándar (por ejemplo, transient, volatile o estructuras complejas).
¿Cuándo NO conviene utilizarlo?
- Si no necesita control total, use Serializable; es más simple y seguro.
- Si no está seguro de poder mantener la compatibilidad del formato de datos cuando cambie la clase.
4. Ventajas y desventajas de Externalizable respecto a Serializable
Ventajas:
- Control total sobre la serialización. Se decide qué y cómo escribir/leer.
- Compacidad. No hay metadatos innecesarios: solo sus datos.
- Velocidad. Menos datos implica escritura/lectura más rápida.
- Flexibilidad. Se puede implementar soporte para distintas versiones del formato, añadir compresión, cifrado, etc.
Desventajas:
- Implementación manual: es fácil equivocarse. Si se confunde el orden de escritura/lectura, la serialización se «romperá» (habrá un error o datos incorrectos).
- No hay soporte automático de transient, serialVersionUID. Todo debe pensarse e implementarse manualmente.
- Más difícil de mantener. Al cambiar la estructura de la clase, hay que acordarse de actualizar los métodos de serialización.
- Constructor público sin parámetros obligatorio.
- Menos «magia» — más responsabilidad.
5. Ejemplos: serialización y deserialización de un objeto sencillo
Serialización de un objeto
User user = new User("Alice", 30);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
Deserialización de un objeto
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println(loaded); // Alice (30)
}
Atención: si se cambia el orden de escritura/lectura de los campos, o se olvida serializar algún campo, ¡los datos serán incorrectos! Los métodos writeExternal y readExternal deben estar estrictamente coordinados en la secuencia de operaciones.
Ejemplo: serializar solo parte de los campos
public class SecretUser implements Externalizable {
private String login;
private transient String password; // transient no tiene efecto en Externalizable
public SecretUser() {}
public SecretUser(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(login);
// ¡No serializamos la contraseña!
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
login = in.readUTF();
password = null; // no se restaura la contraseña
}
}
6. Práctica: comparación del tamaño del archivo serializado
Comparemos cuánto «pesan» los archivos serializados con Serializable y con Externalizable.
Clase con Serializable
public class UserSerializable implements Serializable {
private String name;
private int age;
public UserSerializable(String name, int age) {
this.name = name;
this.age = age;
}
}
Clase con Externalizable
public class UserExternalizable implements Externalizable {
private String name;
private int age;
public UserExternalizable() {}
public UserExternalizable(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF();
age = in.readInt();
}
}
Código para la comparación
import java.io.*;
public class CompareSerialization {
public static void main(String[] args) throws Exception {
UserSerializable s = new UserSerializable("Bob", 25);
UserExternalizable e = new UserExternalizable("Bob", 25);
// Serializable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
out.writeObject(s);
}
// Externalizable
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
out.writeObject(e);
}
System.out.println("Serializable file size: " + new File("ser.bin").length());
System.out.println("Externalizable file size: " + new File("ext.bin").length());
}
}
Resultado:
El archivo ser.bin (Serializable) suele ser mayor: contiene información de servicio de Java. El archivo ext.bin (Externalizable) incluye solo los datos necesarios y, por lo general, es más pequeño.
7. Errores típicos al trabajar con Externalizable
Error n.º 1: ausencia de constructor público sin parámetros.
La clase que implementa Externalizable debe tener obligatoriamente un constructor public sin argumentos. Sin él, la deserialización lanzará InvalidClassException.
Error n.º 2: incumplir el orden de escritura y lectura de los campos.
Los métodos writeExternal y readExternal deben operar en el mismo orden. Si al escribir se guarda primero el campo name y al leer se intenta leer primero age, los datos quedarán corruptos.
Error n.º 3: omitir campos durante la serialización.
Si se olvida escribir un campo en writeExternal, tras la deserialización tendrá el valor null (para tipos de referencia) o 0 (para numéricos).
Error n.º 4: uso incorrecto de transient o serialVersionUID.
A diferencia de Serializable, con Externalizable estos mecanismos no funcionan automáticamente: usted debe controlar qué campos guardar y cuáles no.
Error n.º 5: cambiar la estructura de la clase sin actualizar los métodos.
Si se añaden o eliminan campos y no se introducen los cambios correspondientes en writeExternal y readExternal, los datos guardados anteriormente pueden dejar de cargarse correctamente.
GO TO FULL VERSION