Cuando utilizamos writeObject para escribir un objeto en un stream, estamos realizando una operación conocida como serialización. Esta técnica nos permite convertir instancias de clases Java en una secuencia de bytes que podemos enviar a través de un OutputStream y luego leer con un InputStream. Por ejemplo, es común usar FileOutputStream y FileInputStream para guardar estos datos en archivos, pero la serialización también es muy útil para transmitir objetos entre ordenadores conectados por red, usando sockets TCP y flujos de objetos como ObjectOutputStream.
La idea fundamental es que podemos enviar objetos Java completos a través de la red, permitiendo que un programa en un ordenador reciba y utilice clases generadas en otro servidor remoto. Esto abre muchas posibilidades para la comunicación distribuida y el intercambio de datos complejos.
Sin embargo, no podemos serializar cualquier clase sin más. Para que una clase sea serializable, debe implementar la interfaz Serializable del paquete java.io. Esta interfaz es especial porque no tiene métodos; su presencia simplemente indica a Java que la clase puede ser convertida en bytes. Si intentamos serializar una clase que no implementa esta interfaz, obtendremos un error en tiempo de ejecución, aunque no sea una excepción que debamos capturar explícitamente, ya que es una excepción de tipo RuntimeException.
Por ejemplo, si tenemos una clase Empleado y queremos guardar una lista de empleados en un archivo, debemos asegurarnos de que Empleado implemente Serializable. De lo contrario, al intentar escribir la lista con writeObject, Java nos lanzará un error indicando que Empleado no es serializable.
Una vez que nuestra clase implementa Serializable, podemos guardar objetos como un ArrayList<Empleado> en un archivo. El contenido del archivo no será legible para humanos, ya que incluye información interna sobre la clase, sus campos y sus valores, todo en un formato binario específico de Java. Por ejemplo, veremos que se almacenan los nombres de los campos como apellido, nombre y puesto, junto con sus valores, pero todo ello codificado de forma que solo Java pueda interpretarlo correctamente.
Para leer estos objetos, usamos ObjectInputStream y su método readObject. Esto nos permite recuperar la lista de empleados exactamente como estaba cuando la guardamos. Podemos entonces recorrer la lista y mostrar los datos de cada empleado sin problemas.
Un aspecto crucial a tener en cuenta es que la serialización vincula el formato de la clase con el archivo donde se almacenan los datos. Esto significa que si cambiamos la estructura de la clase después de haber guardado los objetos —por ejemplo, renombrando un campo o eliminándolo— ya no podremos leer esos objetos antiguos sin que se produzca un error. Java valida que la clase que se está leyendo tenga los mismos campos y tipos que la clase original usada para serializar.
Este control se realiza mediante un identificador llamado serialVersionUID, que es una especie de firma generada automáticamente a partir de los nombres y tipos de los campos de la clase. Si la clase cambia, este identificador también cambia, y Java detecta que el archivo no es compatible con la versión actual de la clase, lanzando una excepción.
Podemos definir manualmente un campo serialVersionUID en nuestra clase para mantener la compatibilidad entre versiones, asignándole un valor constante de tipo long. Esto puede ser útil para evitar errores cuando hacemos cambios menores en la clase que no afectan a la estructura de los datos serializados. Sin embargo, esta práctica debe usarse con cuidado, ya que puede llevar a inconsistencias si los cambios en la clase son significativos.
En resumen, la serialización en Java es una herramienta poderosa para almacenar y transmitir objetos, pero requiere que nuestras clases implementen Serializable y que gestionemos con atención los cambios en la estructura de las clases para mantener la compatibilidad con los datos serializados previamente.