Leer y escribir objetos

prev.gif (997 bytes)chapter.gif (1105 bytes)home.gif (1054 bytes)next.gif (998 bytes)

Entrada/salida

El interface Serializable

Lectura/escritura

El modificador transient

Objetos compuestos

La herencia

Serialización personalizada


Java ha añadido una interesante faceta al lenguaje denominada serialización de objetos que permite convertir cualquier objeto cuya clase implemente el interface Serializable en una secuencia de bytes que pueden ser posteriormente leídos para restaurar el objeto original. Esta característica se mantiene incluso a través de la red, por lo que podemos crear un objeto en un ordenador que corra bajo Windows 95/98, serializarlo y enviarlo a través de la red a una estación de trabajo que corra bajo UNIX donde será correctamente reconstruido. No tenemos que procuparnos, en absoluto, de las diferentes representaciones de datos en los distintos ordenadores.

La serialización es una característica añadida al lenguaje Java para dar soporte a  

La invocación remota de objetos permite a los objetos que viven en otros ordenadores comportarse como si vivieran en nuestra propia máquina. La serialización es necesaria para transportar los argumentos y los valores de retorno.

La persistencia, es una característica importante de los JavaBeans. El estado de un componente es configurado durante el diseño. La serialización nos permite guardar el estado de un componente en disco, abandonar el Entorno Integrado de Desarrollo (IDE) y restaurar el estado de dicho componente cuando se vuelve a correr el IDE.

 

El interface Serializable

Un objeto se puede serializar si implementa el interface Serializable. Este interface no declara ninguna función miembro, se trata de un interface vacío.

import java.io.*;
public interface Serializable{
}

Para hacer una clase serializable simplemente ha de implementar el interface Serializable, por ejemplo, a la clase Lista que estudiamos en el capítulo Clases y objetos se le añade la implementación del interface

public class Lista implements java.io.Serializable{
     private int[] x;
     private int n;
//otros miembros...
}

No tenemos que escribir ningún otro método. El método defaultWriteObject de la clase ObjectOutputStream realiza la serialización de los objetos de una clase. Este método escribe en el flujo de salida todo lo necesario para reconstruir dichos objetos:

El método defaultReadObject de la clase ObjectInputStream realiza la deserialización de los objetos de una clase. Este método lee el flujo de entrada y reconstruye los objetos de dicha clase.

 

Lectura/escritura

disco.gif (1035 bytes)archivo4: Lista.java ArchivoApp4.java

Dos flujos de datos ObjectInputStream y ObjectOutputStream están especializados en la lectura y escritura de objetos. El comportamiento de estos dos flujos es similar a sus correspondientes que procesan flujos de datos primitivos DataInputStream y DataOutputStream, que hemos visto en la página previa

Escribir objetos al flujo de salida ObjectOutputStream es muy simple y requiere los siguientes pasos:

  1. Creamos un objeto de la clase Lista
        Lista lista1= new Lista(new int[]{12, 15, 11, 4, 32});
  1. Creamos un fujo de salida a disco, pasándole el nombre del archivo en disco o un objeto de la clase File.
        FileOutputStream fileOut=new FileOutputStream("media.obj");
  1. El fujo de salida ObjectOutputStream es el que procesa los datos y se ha de vincular a un objeto fileOut de la clase FileOutputStream .
       ObjectOutputStream salida=new ObjectOutputStream(fileOut);

o en una sóla línea

       ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("media.obj"));
  1. El método writeObject escribe los objetos al flujo de salida y los guarda en un archivo en disco. Por ejemplo, un string y un objeto de la clase Lista.
            salida.writeObject("guardar este string y un objeto\n");
            salida.writeObject(lista1);
  1. Finalmente, se cierran los flujos
            salida.close();
      Lista lista1= new Lista(new int[]{12, 15, 11, 4, 32});
      ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("media.obj"));
      salida.writeObject("guardar este string y un objeto\n");
      salida.writeObject(lista1);
      salida.close();

El proceso de lectura es paralelo al proceso de escritura, por lo que leer objetos del flujo de entrada  ObjectInputStream es muy simple y requiere los siguientes pasos.

  1. Creamos un fujo de entrada a disco, pasándole el nombre del archivo en disco o un objeto de la clase File.
        FileInputStream fileIn=new FileInputStream("media.obj");
  1. El fujo de entrada ObjectInputStream es el que procesa los datos y se ha de vincular a un objeto fileIn de la clase FileInputStream.
       ObjectInputStream entrada=new ObjectInputStream(fileIn);

o en una sóla línea

       ObjectInputStream entrada=new ObjectInputStream(new FileInputStream("media.obj"));
  1. El método readObject lee los objetos del flujo de entrada, en el mismo orden en el que ha sido escritos. Primero un string y luego, un objeto de la clase Lista.
            String str=(String)entrada.readObject();
            Lista obj1=(Lista)entrada.readObject();
  1. Se realizan tareas con dichos objetos, por ejemplo, desde el objeto obj1 de la clase Lista se llama a la función miembro valorMedio, para hallar el valor medio del array de datos, o se muestran en la pantalla
            System.out.println("Valor medio "+obj1.valorMedio());
            System.out.println("-----------------------------");
            System.out.println(str+obj1);
  1. Finalmente, se cierra los flujos
            entrada.close();
      ObjectInputStream entrada=new ObjectInputStream(new FileInputStream("media.obj"));
      String str=(String)entrada.readObject();
      Lista obj1=(Lista)entrada.readObject();
      System.out.println("Valor medio "+obj1.valorMedio());
      System.out.println("-----------------------------");
      System.out.println(str+obj1);
      System.out.println("-----------------------------");
      entrada.close();

 

El modificador transient

disco.gif (1035 bytes)archivo6: Cliente.java ArchivoApp6.java

Cuando un miembro dato de una clase contiene información sensible, hay disponibles varias técnicas para protegerla. Incluso cuando dicha información es privada (el miembro dato tiene el modificador private) una vez que se ha enviado al flujo de salida alguien puede leerla en el archivo en disco o interceptarla en la red.

El modo más simple de proteger la información sensible, como una contraseña (password) es la de poner el modificador transient delante del   miembro dato que la guarda.

La clase Cliente tiene dos miembros dato, el nombre del cliente y la contraseña o password.

Redefine la función toString miembro de la clase base Object. Esta función devolverá el nombre del cliente y la contraseña. En el caso de que el miembro password guarde el valor null se imprimirá el texto (no disponible).

En el cuadro que sigue se muestra el código que define la clase Cliente.

public class Cliente implements java.io.Serializable{
  private String nombre;
  private transient String passWord;
  public Cliente(String nombre, String pw) {
    this.nombre=nombre;
    passWord=pw;
  }
  public String toString(){
    String texto=(passWord==null) ? "(no disponible)" : passWord;
    texto+=nombre;
    return texto;
  }
}

En el cuadro siguiente se muestra los pasos para guardar un objeto de la clase Cliente en el archivo cliente.obj. Posterioremente, se lee el archivo para reconstruir el objeto obj1 de dicha clase.

  1. Se crea el objeto cliente de la clase Cliente pasándole el nombre del cliente "Angel" y la contraseña "xyz".
  2. Se crea un flujo de salida (objeto salida de la clase ObjectOutputStream) y se asocia con un objeto de la clase FileOutputStream para guardar la información en el archivo cliente.obj.
  3. Se escribe el objeto cliente en el flujo de salida mediante writeObject.
  4. Se cierra el flujo de salida llamando a close.
   Cliente cliente=new Cliente("Angel", "xyz");

   ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("cliente.obj"));
   salida.writeObject("Datos del cliente\n");
   salida.writeObject(cliente);
   salida.close();

Para reconstruir el objeto obj1 de la clase Cliente se procede del siguiente modo:

  1. Se crea un flujo de entrada (objeto entrada de la clase ObjectInputStream) y se asocia con un objeto de la clase FileInputStream para leer la información que gurada el archivo cliente.obj.
  2. Se lee el objeto cliente en el flujo de salida mediante readObject.
  3. Se imprime en la pantalla dicho objeto llamando implícitamente a su función miembro toString.
  4. Se cierra el flujo de entrada llamando a close.
   ObjectInputStream entrada=new ObjectInputStream(new FileInputStream("cliente.obj"));
   String str=(String)entrada.readObject();
   Cliente obj1=(Cliente)entrada.readObject();
   System.out.println("------------------------------");
   System.out.println(str+obj1);
   System.out.println("------------------------------");
   entrada.close();

La salida del programa es

Datos del cliente
(no disponible) Angel

Lo que nos indica que la información sensible guardada en el miembro dato password que tiene por modificador transient no ha sido guardada en el archivo. En la reconstrucción del objeto obj1 con la información guardada en el archivo el miembro dato password toma el valor null.

 

Objetos compuestos

disco.gif (1035 bytes)archivo5: Punto.java, Rectangulo.java ArchivoApp5.java

Volvemos de nuevo al estudio de la clase Rectangulo que contiene un subobjeto de la clase Punto.

A dichas clases se les ha añadido la redefinición de la función toString miembro de la clase base Object (esta redefinición no es necesaria aunque es ilustrativa para explicar el comportamiento de un objeto compuesto). Como podemos apreciar, ambas clases implementan el interface Serializable.

En el cuadro que sigue se muestra parte del código que define la clase Punto.

public class Punto implements java.io.Serializable{
    private int x;
    private int y;
//otros miembros...

  public String toString(){
    return new String("("+x+", "+y+")");
  }
}

La definición de la clase Rectangulo se muestra en el siguiente cuadro

public class Rectangulo implements java.io.Serializable{
    private int ancho ;
    private int alto ;
    private Punto origen;
//otras funciones miembro...
  
    public String toString(){
        String texto=origen+" w:"+ancho+" h:"+alto;
        return texto;
    }
}

Como podemos observar, en la definición de toString de la clase Rectangulo se hace una llamada implícita a la función toString miembro de la clase Punto. La composición como se ha estudiado permite reutilizar el código existente.

Para guardar en un archivo un objeto de la clase Rectangulo hay que seguir los mismos pasos que para guardar un objeto de la clase Lista o de la clase Cliente.

   Rectangulo rect=new Rectangulo(new Punto(10,10), 30, 60);

   ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("figura.obj"));
   salida.writeObject("guardar un objeto compuesto\n");
   salida.writeObject(rect);
   salida.close();

Para reconstruir un objeto de la clase Rectangulo a partir de los datos guardados en el archivo hay que seguir los mismos pasos que en los dos ejemplos previos.

   ObjectInputStream entrada=new ObjectInputStream(new FileInputStream("figura.obj"));
   String str=(String)entrada.readObject();
   Rectangulo obj1=(Rectangulo)entrada.readObject();
   System.out.println("------------------------------");
   System.out.println(str+obj1);
   System.out.println("------------------------------");
   entrada.close();

En el caso de que nos olvidemos de implementar el interface Serializable en la clase Punto que describe el subobjeto de la clase Rectangulo, se lanza una excepción, imprimiéndose en la consola.

java.io.NotSerializableException: archivo5.Punto.

 

La herencia

disco.gif (1035 bytes)archivo8: Figura.java, ArchivoApp8.java

En el apartado anterior hemos examinado la composición, ahora examinemos la herencia. En el capítulo de la herencia examinamos una jerarquía formada por una clase base denominada Figura y dos clases derivadas denominadas Circulo y Rectangulo.

Como podemos observar en el cuadro adjunto se han hecho dos modificaciones. La clase base Figura implementa el interface Serializable y en la clase Circulo en vez de usar el número PI proporcionado por la clase Math, definimos una constante estática PI con una aproximación de 4 decimales. De este modo probamos el comportamiento de un miembro estático en el proceso de serialización.

Para serializar objetos de una jerarquía solamente la clase base tiene que implementar el interface Serializable

public abstract class Figura implements java.io.Serializable{
    protected int x;
    protected int y;
    public Figura(int x, int y) {
        this.x=x;
        this.y=y;
    }
    public abstract double area();
}

class Circulo extends Figura{
    protected double radio;
    private static final double PI=3.1416;
    public Circulo(int x, int y, double radio){
        super(x,y);
        this.radio=radio;
    }
    public double area(){
        return PI*radio*radio;
    }
}

class Rectangulo extends Figura{
    protected double ancho, alto;
    public Rectangulo(int x, int y, double ancho, double alto){
        super(x,y);
        this.ancho=ancho;
        this.alto=alto;
    }
    public double area(){
        return ancho*alto;
    }
}

Vamos a serializar dos objetos uno de la clase Rectangulo y otro de la clase Circulo, y a continuación reconstruiremos dichos objetos. Una vez de que dispongamos de los objetos llamaremos a las funciones area para calcular el área de cada una de las figuras.

Para guardar en el archivo figura.obj un objeto fig1 de la clase Rectangulo y otro objeto fig2 de la clase Circulo, se siguen los mismos pasos que hemos estudiado en apartados anteriores

    Figura fig1=new Rectangulo(10,15, 30, 60);
    Figura fig2=new Circulo(12,19, 60);

    ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("figura.obj"));
    salida.writeObject("guardar un objeto de una clase derivada\n");
    salida.writeObject(fig1);
    salida.writeObject(fig2);
    salida.close();

Fijarse que fig1 y fig2 son dos referencias de la clase base Figura en la que se guardan objetos de las clases derivadas Rectangulo y Circulo, respectivamente

Para leer los datos guardados en el archivo figura.obj y reconstruir dos objetos obj1 y obj2 de las clases Rectangulo y Circulo respectivamente, se procede de forma similar a la estudiada en los apartados previos.

    ObjectInputStream entrada=new ObjectInputStream(new FileInputStream("figura.obj"));
    String str=(String)entrada.readObject();
    Figura obj1=(Figura)entrada.readObject();
    Figura obj2=(Figura)entrada.readObject();
    System.out.println("------------------------------");
    System.out.println(obj1.getClass().getName()+" origen ("+obj1.x+", "+obj1.y+")"+" area="+obj1.area());
    System.out.println(obj2.getClass().getName()+" origen ("+obj2.x+", "+obj2.y+")"+" area="+obj2.area());
    System.out.println("------------------------------");
    entrada.close();

Fijarse que obj1 y obj2 son referencias a la clase base Figura. Sin embargo, cuando obj1 llama a la función area nos devuelve (correctamente) el área del rectángulo y cuando, obj2 llama a la función area devuelve el área del círculo.

Fijarse también que aunque PI es un miembro estático de la clase Circulo, se reconstruye el objeto obj2 con el valor del miembro estático con el que se calcula el área del círculo

 

Serialización personalizada

El proceso de serialización proporcionado por el lenguaje Java es suficiente para la mayor parte de las clases, ahora bien, se puede personalizar para aquellos casos específicos.

Para personalizar la serialización, es necesario definir dos funciones miembros writeObject y readObject. El primero, controla que información es enviada al flujo de salida. La segunda, lee la información escrita por writeObject .

La definición de writeObject ha de ser la siguiente

private void writeObject (ObjectOutputStream s) throws IOException{
	s.defaultWriteObject();
	//...código para escribir datos
}

La función readObject ha de leer todo lo que se ha escrito con writeObject en el mismo orden en el que se ha escrito. Además, puede realizar otras tareas necesarias para actualizar el estado del objeto.

private void readObject (ObjectInputStream s) throws IOException{
	s.defaultReadObject();
	//...código para leer datos
	//...
	//actualización del estado del objeto, si es necesario
}

Para un control explícito del proceso de serialización la clase ha de implementar el interface Externalizable. La clase es responsable de escribir y de leer su contenido, y ha de estar coordinada con sus calses base para hacer esto.

La definición del interface Externalizable es la siguiente

packege java.io;

public interface Externalizable extends Serializable{
   public void writeExternal(ObjectOutput out) throws IOException;
   public void readExternal(ObjectOutput in) throws IOException, java.lang.ClassNotFoundException;;
}