Está en la página 1de 7

SERIALIZACION EN JAVA

En esta entrada vamos a ver lo referente a la Serialización en Java, que es, para que se usa y como podemos

aplicarla.

Imaginemos que queremos guardar el estado de uno o mas objetos. Si java no tuviera la serialización (como
las primeras versiones no tenían), tendríamos que haber usado algunas de las clases de I/O para escribir el
estado de las variables de instancia de todos los objetos que quisieramos guardar. La peor parte de esto sería
reconstruir todos estos objetos nuevos que serían virtualmente identicos a los objetos que estabamos
intentando guardar. Necesitaríamos nuestro propio protocolo para la manera en la cual lo escribimos y en la
que restauramos el estado de cada objeto, o podríamos establecer variables con valores incorrectos. Por
ejemplo, imaginemos ue hemos guardado un objeto que tiene unas variables de instancia para el ancho y la
altura de algo. Cuando los guardamos el estado del objeto, podríamos escribir la altura y el ancho como 2 int
en un fichero, pero el orden en el que lo hemos escrito es crucial. Sería muy facil recrear el objeto pero a la
vez sería muy facil equivocarse con las variables, y asignar el ancho a la altura o viceversa.La serialización
nos permite decir “Guarda este objeto y todas sus variables de instancia.” Actualmente es algo mas
interesante que esto, porque podemos agregar, “… a no ser que una variable sea marcada como transient, lo
que significa, que no se incluye el valor de las variables marcadas como transient como parte del estado del
objeto serializado”.
 Trabajando con ObjectOutputStream y ObjectInputStream

La magia de la serializacion ocurre con solo 2 métodos: uno para serializar objetos y escribirlos en un stream,

y el segundo para leer el stream y deserializar los objetos.


1 ObjectOutputStream.writeObject();   // Serializa y escribe
2 ObjectInputStream.readObject();     // Lee y deserializa
3

Las clases java.io.ObjectOutputStream y java.io.ObjectInputStream son consideradas clases de alto nivel en

el package java.io, y como vimos con anterioridad en la anterior sección, esto significa que las envolveremos

en clases de bajo nivel, como java.io.FileOutputStream o java.io.FileInputStream. Aquí tenemos un pequeño

programa que crea un objeto, lo serializa, y luego lo deserializa:


1
import java.io.*;
2
3  
class Cat implements Serializable { }   // 1
4
5  
public class SerializeCat {
6
    public static void main (String[] args){
7
        Cat c = new Cat();      // 2
8
        try {
9             FileOutputStream fs = new FIleOutputStream("testSer.ser");
10             ObjectOutputStream os = new Obje
11             c.OutputStream(fs);
12             os.writeObject(c);      // 3
13             os.close()
14         }catch(IOException e){ e.printStackTrace(); }
15          
16         try{
17             FileInputStream fim = new FileInputStream("testSer.ser");
18             ObjectInputStream ois = new ObjectInputStream(fim);
19             c = (Cat)ois.readObject();  // 4
20             ois.close();
21         } catch (IOException ex){ e.printStackTrace(); }
22     }
23 }
24
Vamos a ver los puntos clave de este ejemplo:

1. Hemos declarado una clase Cat la cual implementa la interface Serializable. Es una
interface que marca, no tiene métodos que implementar.
2. Hemos creado un objeto Cat, el cual como sabemos es serializable.
3. Hemos serializado el objeto Cat cuya variable de referencia es c invocando el método
writeObject(). Se requiere una preparación anterior antes de que podamos serializar nuestro Cat. Primero
tenemos que poner todo el código relacionado con la operación I/O en un bloque try/catch. A continuación
tenemos que crear un FileOutputStream para escribir el objeto. Despues hemos envuelto el objeto
FileOutputStream en un ObjectOutputStream, el cual es la clase que tiene el método mágico de serialización
que necesitamos. Recordemos que la invocación de writeObject() realiza 2 tareaas: serializa el objeto, y
entonces escribe el objeto serializado en un fichero.
4. De-serializamos el objeto Cat invocando el método readObject(). Este método retorna un
objeto, por lo que tenemos que hacer un cast al objeto que hemos deserializado a Cat. De nuevo, tenemos
que hacer todo lo relacionado con las operaciones I/O, bloque try/catch, etc.
Este es un ejemplo muy básico para comprobar la serialización en acción, y apenas tiene dificultad. En las
siguientes secciones vamos a ver ejemplos mas complejos y que nos pueden dar problemas relacionados con
la serialización.

 Gráficas de Objetos / Object Graphs

¿Qué significa realmente guardar un objeto?. Si las vriables de instancia son todas de tipos primitivos, es muy
fácil. Pero ¿Qué pasa si las variables de instancia son referencias a otros objetos? ¿Qué se salva?.
Claramente en Java no tendría sentido salvar el valor actual de las variables de referencia, porque son valores
de una referencia en Java que tiene sentido solo en el contexto de una singular JVM, incluso ejecutandose en
el mismo ordenador en el cual el objeto fuera originalmente serializado, la referencía no tendría utilidad. Pero
¿Qué pasa con el objeto al cual la referencia se refiere? Veamos esta clase:
1
class Dog {
2
    private Collar theCollar;
3
    private int dogSize;
4
    public Dog(Collar, int size) {
5
        theCollar = collar;
6
        dogSize = size;
7
    }
8     public Collar getCollar() { return theCollar; }
9 }
10 class Collar {
11     private int collarSeize;
12     public Collar (int size) { collarSize = size; }
13     public int getCollarSize() { return collarSize; }
14 }
15 [/sourcecode[]
16 <p>Ahora hacemos un objeto Dog, pero primero hacemos un collar para el perro:</p>
17  
18 Collar c = new Collar(3);
19

Entonces hacemos un Dog, pasandole el Collar:

1 Dog d = new Dog(c, 8);


2
Entonces ¿Qué pasa cuando guardamos el objeto Dog? Si el propósito es guardar y luego restaurar el objeto
Dog, y el Dog retaurado es un duplicado exacto del objeto Dog que fué salvado, entonces el Dog necesita un
Collar que sea exactamente duplicado al Collar del Dog que fué guardado. Esto significa que ambos, Dog y
Collar tendrían que ser salvados.
Y ¿Qué pasa si el Collar por sí mismo tiene referencias a otros objetos, como por ejemplo un objeto Color?
Esto se ha complicado muy rápido. Si el programador supiera toda la estructura interna de todos los objetos a
los que refiere Dog, el programador podría estar seguro de guardar todos los estados de todos los objetos.
Esto sería una pesadilla incluso para los objetos mas simples.
Afortunadamente, el mecanismo de serialización en Java tiene cuidado de todo esto. Cuando serialiamos un
objeto, la serialización de Java toma el cuidado de guardar todo el objeto entero o “Gráfica del objeto / Object
Graph”. Esto significa que se copiaría todo aquello que el objeto salvado necesita para ser restaurado. Por
ejemplo, si serializamos el objeto Dog, el Collar se serializaría automáticamente. Y si la clase Collar contenía
una referencia a otro objeto, ESE objeto sería también serializado, y así con todo. Y el único objeto del cual
nos tendríamos que preocupar para salvar y restaurar sería el Dog. Los otros objetos requeridos para
reconstruir completamente el Dog son salvados (y restaurados) automáticamente a través de la serialización.
Recordemos, tenemos que hacer una elección consciente para crear objetos que son serializable,
implementando la interface Serializable. Si queremos salvar objetos Dog, por ejemplo, tendremos que
modificar la clase Dog de la siguiente manera:
1 class Dog implements Serializable{
2     // El resto de código de antes
3
     
4
    // Serializable no requiere implementar métodos
5 }
6

Y ahora podemos salvar Dog como en el siguiente código:

1
import java.io.*;
2
public class SerializeDog {
3
    public static void main(String[] args){
4
        Collar c = new Collar(3);
5
        Dog d = new Dog(c, 8);
6         try{
7             FileOutputStream fs = new FileOutputStream("testSer.ser");
8             ObjectOutputStream os = new ObjectOutputStream(fs);
9             os.writeObject(d);
10             os.close();
11         }catch (Exception ex){ ex.printStackTrace(); }
12     }
13 }
14

Pero cuando ejecutamos este código tendremos una excepción en tiempo de ejecución que se parecería a lo

siguiente:

1 java.io.NotSerializableException: Collar
2

¿Qué hemos olvidado? La clase Collar debe tambien ser Serializable. Si modificamos la clase Collar y la

hacemos serializable, entonces no habría problemas:

1 class Collar implementes Serializable{


2     // Lo mismo
3 }
4

Aquí tendríamos el listado completo:

1
2 import java.io.*;
3 public class SerializeDog{
4     public static void main (String[] args){
5         Collar c = new Collar(3);
6         Dog d = new Dog(c, 5);
        System.out.println("before: collar size is " + d.getCollar().getCollarSize());
7
        try{
8
            FileOutputStream fs = new FileOutputStrem("testSer.ser");
9
            ObjectOutputStream os = new ObjectOutputStream(fs);
10
            os.writeObject(d);
11
            os.close();
12
        } catch (Exception e) {e.printStackTrace(); }
13
         
14
        try {
15
            FileInputStream fis = new FileInputStream("testSer.ser");
16
            ObjectInputStream ois = new ObjectInputStream(ois);
17
            d = (Dog)ois.readObject();
18
            ois.close();
19
        } catch(Exception e) { e.printStackTrace(); }
20
         
21
        System.out.println("after: collar size is " + d.getCollar().getCollarSize());
22
    }
23
}
24
class Dog implements Serializable {
25
    private Collar theCollar;
26
    private int dogSize;
27     public Dog (Collar collar, int size){
28         theCollar = collar;
29         dogSize = size;
30     }
31     public Collar getCollar(){
32         return theCollar;
33     }
34 }
35 class Collar implements Serializable {
36     private int collarSize;
37     public Collar(int size){
38         collarSize = size;
39     }
40     public int getCollarSize() {
41         return collarSize;
42     }
43 }
44

Esto produce la salida:

1 before: collar size is 3


2 after: collar size is 3
3
Pero ¿Qué pasaría si no tuvieramos acceso al código fuente de la clase Collar? En otras palabras, si hacer la
clase Collar serializable no fuera una opción. ¿Estamos parados en un Dog que no puede ser Serializable?.
Obviamente podríamos hacer una subclase de la clase Collar, marcar la subclase como Serializable, y
entonces usar la subclase COllar en vez de la clase Collar. Pero no es siempre una opción por varias razones
potenciales:
1. La clase Collar puede ser final, para prevenir hacer subclases.
2. La clase Collar puede por sí misma referir a otros objetos no serializabls, y sin saber la
estructura de Collar, no podríamos hacer estos ajustes.
3. Hacer subclases no es una opción por otras razones en relación a nuestro diseño.
Por lo que…¿Qué hacemos si queremos guardar la clase Dog?.
Aquí es donde viene el modificador transient. Si marcamos la variable de instancia Collar como transient,
entonces la serialización simplemente omite Collar durante la serialización:
1
class Dog implements Serializable{
2
    private transient Collar theCollar;     // Añade el modificador
3     // El resto de la clase es igual
4 }
5
 
6 class Collar {
7     // el mismo código
8 }
9

Ahora tenemos un Dog serializable, con un objeto Collar no serializable, pero el Dog tiene Collar como

transient; la salida es:

1 before: collar size is 3


2 Exception in thread "main" java.lang.NullPointerException
3

¿Y ahora qué podemos hacer?

 Usando writeObject y readObject

Consideramos el problema: tenemos un objeto Dog que queremos salvar. El Dog tiene Collar, y el estado de
Collar que debería tambien ser guardado es parte del estado de Dog. Pero…el Collar no es serializable, por lo
que lo hemos marcado como transient. Esto significa que cuando Dog es deserializado, viene con el un
Collar cuyo valor es null. ¿ue podemos hacer para estar eguros de que Dog es deserializado y obtiene un
Collar que coincida con el que Dog tenía cuando fué salvado?. La serialización en Java tiene unos mecanimos
especiales para esto, un set de métodos privados que podemos implementar en nuestra clase, y si están
presentes, serán invocados automáticamente durante la serialización y la deserialización. Es como si estos
métodos estuvieran definidos en la interface Serializable, excepto que no lo están. Son parte de una llamada
especial junto con el sistema de serialización que básicamente dice, “Si tienes un par de métodos que se
llaman igual, estos métodos serán llamados durante el proceso de la serialización/deserialización”.
Estos métodos nos permiten un paso en la mitad de la serialización y deserialización. Por esto es perfecto
para permitir solventar el problema de Dog/Collar: cuando un Dog está siendo salvado, podemos entrar en el
medio de la serialización y decir, “Me gustaría guardar el estado de la variable Collar (un int) en el stream
cuando el Dog sea serializado”. Hemos añadido manualmente el estado de Collar a la representación de Dog
serializada, incluso aunque el Collar por sí mismo no sea salvado.
Por suuesto, necesitarás restaurar el Collar durante la deserialización entrando en la mitad del proceso y
diciendo, “Leeré un int extra que salvé en el stream de Dog, y lo uso para crear un nuevo Collar, y entonces
se lo asigno al nuevo Collar del Dog que se está deserializando”. Los 2 métodos especiales que definimos
tienen que tener el mismo nombre, EXACTAMENTE el siguiente:
1 private void writeObject(ObjectOutputStream os){
2     // Cödigo para guardar las variables de collar
3
}
4
 
5 private void readObject(ObjectInputStream is){
6     // El código para leer el estado de Collar, crear un nuevo Collar
7     // y asignarlo a Dog
8 }
9

Si, estamos escribiendo métodos con el mismo nombre que los que hemos estado llamando. ¿Dónde van

estos métodos? Vamos a cambiar la clase Dog:

1
2 class Dog implements Serializable{
3     transient private collar theCollar; // No podemos seralizar esto
    private int dogSize;
4
    public Dog(Collar collar, int size){
5
        theCollar = collar;
6
        dogSize = size;
7
    }
8
    public Collar getCollar(){
9
        return theCollar;
10
    }
11
    private void writeObject(ObjectOutputStream os){
12         // throws IOException{
13         try{
14             os.defaultWriteObject();
15             os.writeInt(theCollar.getCollarSize());
16         }catch (Exception e) { e.printStackTrace(); }
17     }
18     private void readObject(ObjectInputStream is){
19         // throws IOException, ClassNotFoundException
20         try{
21             is.defaultReadObject();
22             theCollar = new Collar(is.readInt());
23         } catch (Exception e) { e.printStackTrace(); }
24     }
25 }
26

Vamos a echar un vistazo al ejemplo anterior.

En nuestro escenario hemos agregado que, por alguna razón del mundo, no podemos serializar el objeto
Collar, pero queremos serializar Dog. Para hacer esto vamos a implementar los métodos writeObject() y
readObject(). Implementando estos métodos estamos diciendole al compilador: “Si algo invoca a writeObject()
o readObject() que concierne con el objeto Dog, usa esta parte del código de para leer y escribir”-

1. Como todos los métodos relacionados con procesos de I/O, writeObject puede lanzar

exceptiones. Podemos declararlas o manejarlas pero es recomendable manejarlas.


2. Cuando invocamos defaultWriteObject() dentro de writeObject() le estamos diciendo a la
JVM que haga el proceso normal de serialización para esto objeto. Cuando implementamos writeObject(),
estamos haciendo el proceso normal de serialización, y algo mas personalizado a la hora de leer y escribir.
3. En este caso hemos decidido escribir un int extra (el tamaño del collar) en el stream que
está creando el Dog serializado. Podemos escribir mas cosas antes o despues de invocar el método
defaultWriteObject(). PERO…cuando lo leamos, debemos leer todo aquello que hayamos agregado extra en
el mismo orden que el que hemos escrito.
4. De nuevo, elegimos manejar las excepciones en vez de declararlas.
5. Cuando es tiempo de deserializar, defaultReadObject() maneja la deserialización
normalmente como si no hubieramos implementado el método readObject().
6. Finalmente hemos construido un nuevo objeto Collar para el Dog usando la variable int que
manualmente serializamos. (Tenemos que invocar readInt() después de invocar defaultReadObject() o la
información podría desincronizarse).
Recordemos, que la razón mas común para implementar writeObject() y readObject() es cuando tenemos que
guardar alguna parte del estado de un objeto manualmente. Podemos elegir y escribir y leer TODO el estado
por nosotros mismos, pero es muy raro. Por lo que cuando queremos hacer que una parte del proceso de
serialización/deserialización sea hecha por nosotros, DEBEMOS invocar los métodos defaultWriteObject() y
defaultReadObject() para hacer el resto.
Esto nos trae otra cuestión, ¿Por qué no todas las clases de Java son serializables? ¿Por qué la clase Object
no es serializable? Hay cosas en Java que simplemente no pueden ser serializadas porque son específicas
en tiempo de ejecución. Cosas como streams, threads, runtime, etc, e incluso algunas clases para las GUI no
pueden ser serializables. Lo que es o no es serializable en la API de Java son cosas que tenemos que tener
en mente si queremos serializar objetos complejos.

También podría gustarte