![]() ![]() ![]() ![]() |
Entrada/salida |
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.
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.
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:
Lista lista1= new Lista(new int[]{12, 15, 11, 4, 32});
FileOutputStream fileOut=new FileOutputStream("media.obj");
ObjectOutputStream salida=new ObjectOutputStream(fileOut);
o en una sóla línea
ObjectOutputStream salida=new ObjectOutputStream(new FileOutputStream("media.obj"));
salida.writeObject("guardar este string y un objeto\n"); salida.writeObject(lista1);
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.
FileInputStream fileIn=new FileInputStream("media.obj");
ObjectInputStream entrada=new ObjectInputStream(fileIn);
o en una sóla línea
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);
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(); |
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.
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:
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.
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.
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
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;; }