Serializando clases
Para que ObjectOutputStream nos permita escribir cualquier clase propia que hayamos fabricado, y para poder recuperarla luego con ObjectInputStream, tendremos que agregarle la interfaz Serializable a nuestras clases. Cuando hagamos eso, apreciaremos que en el stream de salida se envía una información interesante.
Cuando nosotros describimos con writeObject() un objeto en un String, lo que estamos haciendo es usar una operación que se conoce como serialización. La serialización nos va a permitir volcar una serie de instancias de objetos Java tal cual como si fuesen una serie de bytes que vamos a poder enviar a través de un OutputStream y leer a través de un InputStream.
En este caso, por ejemplo, siempre lo estoy haciendo con FileOutputStream y con FileInputStream para poder volcarlo sobre archivos, pero tened en cuenta que este tipo de estrategias nos pueden servir, por ejemplo, para hacer que dos ordenadores conectados a través de internet que estén ejecutando un programa Java puedan utilizar un socket TCP con un servidor escrito en Java, y a través del OutputStream de ese socket, podrían intercambiarse una serie de objetos. Esto permitiría transmitir clases a través de la red, lo que puede servir para que un ordenador conectado a la red instancie clases que han sido generadas en otra parte de la red. En eso consiste la serialización: en transmitir de forma binaria clases. Sin embargo, tenemos que tener en cuenta que no podemos hacerla así sin más y que tiene su propio conjunto de normas que tenemos que aplicar. Por ejemplo, en este caso he fabricado una función extra llamada escribirEmpleados() donde voy a tartar de volcar en este OutputStream una serie de elementos metidos en un ArrayList llamados empleados, que son instancias de una clase que he fabricado llamada Empleado.
Si yo trato de ejecutarlo sin más, sorprendentemente lo que voy a obtener es un error, porque me va a decir que streams.Empleado no es serializable. Y es curioso porque esto falla a pesar de que aquí le he puseto un try-catch. Esto es porque esta clase es un poco más interna y no es de tipo IOException, de hecho es de tipo Runtime porque no la tenemos que tratar, pero es importante tenerlo en cuenta. Nosotros no podemos guardar con writeObject() cualquier tipo de clase así sin más, sino que tiene que ser una clase Serializable. Esto lo podemos hacer implementando en nuestra clase original una clase llamada Serializable, que está en el paquete java.io, que es una clase un poco especial, porque es una interfaz especial que está vacía y no tiene nada. Sin embargo, el hecho de que esta interfaz exista, implica que va a existir un poco de magia cuando llamemos a writeObject() porque Java va a interceptar este serializable, y va a decir Ah, esto sí se puede guardar
. De hecho, si nos fijamos en cómo funciona ArrayList, veremos que en la declaración de su clase implementa Serializable.Casi cualquier clase de Java que se pueda serializar va a implementar Serializable, y ya vienen con esa clase de serie. Esta es la razón por la que inicialmente podemos guardar un ArrayList. Pero ahora que he hecho de Empleado una clase serializable, se va a hacer correctamente, y se va a haber creado un archivo llamado empleados.txt que dentro va a contener nuestros empleados de una forma un poco peculiar. Hay mucho ruido porque tenemos java.util.ArrayList, pero también streams.Empleado para representar la clase de Empleado, y no sólo eso, que también tendrá la audacia de guardar los campos que tenemos: apellido de tipo java.lang.String, tenemos nombre, puesto...
Solo aparece una única vez String porque se ocupa de comprimir internamente y por eso hay referencias que no se ven. Y vemos palabras como verde
, Enrique
, comercial
, Sánchez
, Ana
... Vemos que todo está guardado pero de una forma imposible de comprender para una persona que no haya perdido tiempo viendo cómo funciona el formato Serializable. No hagáis como yo: no vale la pena aprenderlo.
Lo importante en este caso es que ahora, si utilizo la función leerEmpleados() voy a poder leerlos correctamente. No es ningún tipo de broma, debería quedar bastante claro que si uso un bloque try-with, y trato de leer usando FileInputStream ese archivo, y luego con ObjectInputStream trato de leerlo, en principio yo ahí voy a tener la posibilidad de sacarme mi lista usando por ejemplo lista es... ois, si no he cambiado el nombre de ninguna variable. Ahora podría leer mi lista, y podría hacer un bucle for para imprimir cada uno de los empleados de mi lista.
Esto puede que no se haya inicializado así que voy a darle por si acaso un valor por defecto. Esto podría lanzar una excepción así que se la voy a plantar ahí. Si ahora llamo a leerEmpleados() voy a poder leer los datos de mis empleados. Como antes: cuatro empleados, con los datos de esos empleados. Eso es como funciona, en principio. Nosotros lo podemos mejorar a partir de ahí, y podemos decirle que ciertas cosas no se escriban. Podemos cambiar el formato, pero tenéis que tener en cuenta una cosa importante, super esencial: cuando nosotros implementamos Serializable, estamos casando el formato de nuestra clase con el del archivo que tenemos ahí. No puedo simplemente renombrar a apellido como apellidos, o cambiar un campo. Si renombrase apellido por apellidos, y asumiendo que no haya que cambiar ninguna otra cosa, ya no puedo leer. Este es el principal problema que tiene ObjectInputStream: como ya os he dicho antes, se ha guardado el contenido y se ha puesto un campo apellido y un campo nombre. Los nombres de los campos se vuelcan en el archivo, lo que significa que cuando hagamos una llamada a readObject(), Java no solo lee, sino que valida que lo que se está leyendo tiene sentido. No solo que exista una clase Empleado, sino que existan los campos nombre, apellido y puesto, y esto es importante porque significa que si cambiamos un campo o si este desaparece, ya no tenemos la posibilidad de leerlo otra vez.
Nos dirá que el serialVersionUID de la clase ha cambiado. Esto es un identificador que se puede generar sobre la marcha, que es una firma de la clase generada a partir del nombre de todos los campos. Aunque es verdad que nosotros podemos sobre escribirlo utilizando una variable global pública de tipo long llamada serialVersionUID, como esta. Esto nos permite sobreescribirlo para que siempre tenga la misma firma y no cambie, y eso nos dejaría engañar al sistema. Hasta cierto punto, la mayoría de clases de la biblioteca estándar de Java van a definir su propio serialVersionUID. Pero sin embargo, de cualquier modo esto no es más que hacer trampas. Aquí la lección que quiero contaros es la siguiente.
Desplegar transcripción del episodio
[Música]
Cuando nosotros describimos con writeObject() un objeto en un String, lo que estamos haciendo es usar una operación que se conoce como serialización. La serialización nos va a permitir volcar una serie de instancias de objetos Java tal cual como si fuesen una serie de bytes que vamos a poder enviar a través de un OutputStream y leer a través de un InputStream.
En este caso, por ejemplo, siempre lo estoy haciendo con FileOutputStream y con FileInputStream para poder volcarlo sobre archivos, pero tened en cuenta que este tipo de estrategias nos pueden servir, por ejemplo, para hacer que dos ordenadores conectados a través de internet que estén ejecutando un programa Java puedan utilizar un socket TCP con un servidor escrito en Java, y a través del OutputStream de ese socket, podrían intercambiarse una serie de objetos. Esto permitiría transmitir clases a través de la red, lo que puede servir para que un ordenador conectado a la red instancie clases que han sido generadas en otra parte de la red. En eso consiste la serialización: en transmitir de forma binaria clases. Sin embargo, tenemos que tener en cuenta que no podemos hacerla así sin más y que tiene su propio conjunto de normas que tenemos que aplicar. Por ejemplo, en este caso he fabricado una función extra llamada escribirEmpleados() donde voy a tartar de volcar en este OutputStream una serie de elementos metidos en un ArrayList llamados empleados, que son instancias de una clase que he fabricado llamada Empleado.
Si yo trato de ejecutarlo sin más, sorprendentemente lo que voy a obtener es un error, porque me va a decir que streams.Empleado no es serializable. Y es curioso porque esto falla a pesar de que aquí le he puseto un try-catch. Esto es porque esta clase es un poco más interna y no es de tipo IOException, de hecho es de tipo Runtime porque no la tenemos que tratar, pero es importante tenerlo en cuenta. Nosotros no podemos guardar con writeObject() cualquier tipo de clase así sin más, sino que tiene que ser una clase Serializable. Esto lo podemos hacer implementando en nuestra clase original una clase llamada Serializable, que está en el paquete java.io, que es una clase un poco especial, porque es una interfaz especial que está vacía y no tiene nada. Sin embargo, el hecho de que esta interfaz exista, implica que va a existir un poco de magia cuando llamemos a writeObject() porque Java va a interceptar este serializable, y va a decir Ah, esto sí se puede guardar
. De hecho, si nos fijamos en cómo funciona ArrayList, veremos que en la declaración de su clase implementa Serializable.Casi cualquier clase de Java que se pueda serializar va a implementar Serializable, y ya vienen con esa clase de serie. Esta es la razón por la que inicialmente podemos guardar un ArrayList. Pero ahora que he hecho de Empleado una clase serializable, se va a hacer correctamente, y se va a haber creado un archivo llamado empleados.txt que dentro va a contener nuestros empleados de una forma un poco peculiar. Hay mucho ruido porque tenemos java.util.ArrayList, pero también streams.Empleado para representar la clase de Empleado, y no sólo eso, que también tendrá la audacia de guardar los campos que tenemos: apellido de tipo java.lang.String, tenemos nombre, puesto...
Solo aparece una única vez String porque se ocupa de comprimir internamente y por eso hay referencias que no se ven. Y vemos palabras como verde
, Enrique
, comercial
, Sánchez
, Ana
... Vemos que todo está guardado pero de una forma imposible de comprender para una persona que no haya perdido tiempo viendo cómo funciona el formato Serializable. No hagáis como yo: no vale la pena aprenderlo.
Lo importante en este caso es que ahora, si utilizo la función leerEmpleados() voy a poder leerlos correctamente. No es ningún tipo de broma, debería quedar bastante claro que si uso un bloque try-with, y trato de leer usando FileInputStream ese archivo, y luego con ObjectInputStream trato de leerlo, en principio yo ahí voy a tener la posibilidad de sacarme mi lista usando por ejemplo lista es... ois, si no he cambiado el nombre de ninguna variable. Ahora podría leer mi lista, y podría hacer un bucle for para imprimir cada uno de los empleados de mi lista.
Esto puede que no se haya inicializado así que voy a darle por si acaso un valor por defecto. Esto podría lanzar una excepción así que se la voy a plantar ahí. Si ahora llamo a leerEmpleados() voy a poder leer los datos de mis empleados. Como antes: cuatro empleados, con los datos de esos empleados. Eso es como funciona, en principio. Nosotros lo podemos mejorar a partir de ahí, y podemos decirle que ciertas cosas no se escriban. Podemos cambiar el formato, pero tenéis que tener en cuenta una cosa importante, super esencial: cuando nosotros implementamos Serializable, estamos casando el formato de nuestra clase con el del archivo que tenemos ahí. No puedo simplemente renombrar a apellido como apellidos, o cambiar un campo. Si renombrase apellido por apellidos, y asumiendo que no haya que cambiar ninguna otra cosa, ya no puedo leer. Este es el principal problema que tiene ObjectInputStream: como ya os he dicho antes, se ha guardado el contenido y se ha puesto un campo apellido y un campo nombre. Los nombres de los campos se vuelcan en el archivo, lo que significa que cuando hagamos una llamada a readObject(), Java no solo lee, sino que valida que lo que se está leyendo tiene sentido. No solo que exista una clase Empleado, sino que existan los campos nombre, apellido y puesto, y esto es importante porque significa que si cambiamos un campo o si este desaparece, ya no tenemos la posibilidad de leerlo otra vez.
Nos dirá que el serialVersionUID de la clase ha cambiado. Esto es un identificador que se puede generar sobre la marcha, que es una firma de la clase generada a partir del nombre de todos los campos. Aunque es verdad que nosotros podemos sobre escribirlo utilizando una variable global pública de tipo long llamada serialVersionUID, como esta. Esto nos permite sobreescribirlo para que siempre tenga la misma firma y no cambie, y eso nos dejaría engañar al sistema. Hasta cierto punto, la mayoría de clases de la biblioteca estándar de Java van a definir su propio serialVersionUID. Pero sin embargo, de cualquier modo esto no es más que hacer trampas. Aquí la lección que quiero contaros es la siguiente.