Objetos serializables

Para que un programa java pueda convertir un objeto en un montón de bytes y pueda luego recuperarlo, el objeto necesita ser Serializable. Al poder convertir el objeto a bytes, ese objeto se puede enviar a través de red, guardarlo en un fichero, y después reconstruirlo al otra lado de la red, leerlo del fichero,….

Para que un objeto sea serializable basta con que implemente la interfaz Serializable. Como la interfaz Serializable no tiene métodos, es muy sencillo implementarla, basta con un implements Serializable y nada más. Por ejemplo, la clase Datos siguiente es Serializable y java sabe perfectamente enviarla o recibirla por red, a través de socket o de rmi. También java sabe escribirla en un fichero o reconstruirla a partir del fichero.

public class Datos implements Serializable { 
  public int a; 
  public String b; 
  public char c; 
}

Si dentro de la clase hay atributos que son otras clases, éstos a su vez también deben ser Serializable. Con los tipos de java (String, Integer, etc.) no hay problema porque lo son. Si ponemos como atributos nuestras propias clases, éstas a su vez deben implementar Serializable. Por ejempl:

/* Esta clase es Serializable porque implementa Serializable y todos sus
 *  campos son Serializable, incluido "Datos f;"
 */
public class DatoGordo implements Serializable
{
   public int d;
   public Integer e;
   Datos f;
}

En realidad, se llama “serializar un objeto” al proceso de convertirlo a bytes, para poder enviarlo por una red, y reconstruirlo luego a partir de esos bytes.

A veces podemos necesitar que se haga algo especial en el momento de serializarlo, bien al construirlo a bytes, bien al recibirlo. Por ello java nos permite hacerlo. Basta con hacer que nuestro objeto defina uno o los dos métodos siguientes:

private void readObject(java.io.ObjectInputStream stream)
     throws IOException, ClassNotFoundException
{
   // Aqui debemos leer los bytes de stream y reconstruir el objeto
}

private void writeObject(java.io.ObjectOutputStream stream)
     throws IOException
{
   // Aquí escribimos en stream los bytes que queramos que se envien por red.
}

java llamará a estos métodos cuando necesite convertirlo a bytes para enviarlo por red o cuando necesite reconstruirlo a partir de los bytes en red.

Convertir un Serializable a byte[] y viceversa

Podemos convertir cualquier objeto Serializable a un array de byte y viceversa. Normalmente esto no es necesario que lo hagamos explícitamente en el código para enviar el objeto por un socket o escribirlo en un fichero puesto que contamos con las clases ObjectInputStream y ObjectOutputStream que se encargan de ello. Sin embargo, en ocasiones, por ejemplo, al intentar enviar un objeto por un socket udp, sí es necesario hacerlo manualmente.

Para hacer esta conversión, podemos usar este código

De objeto a byte[]

ByteArrayOutputStream bs= new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream (bs);
os.writeObject(unObjetoSerializable);  // this es de tipo DatoUdp
os.close();
byte[] bytes =  bs.toByteArray(); // devuelve byte[]

De byte[] a objeto

ByteArrayInputStream bs= new ByteArrayInputStream(bytes); // bytes es el byte[]
ObjectInputStream is = new ObjectInputStream(bs);
ClaseSerializable unObjetoSerializable = (ClaseSerializable)is.readObject();
is.close();

Una utilidad de estos dos códigos es el realizar copias “profundas” de un objeto, es decir, se obtiene copia del objeto, copias de los atributos y copias de los atributos de los atributos. Basta convertir el objeto a byte[] y luego reconstruirlo en otra variable.

Serial Version UID

Cuando pasamos objetos Serializable de un lado a otro tenemos un pequeño problema. Si la clase que queremos pasar es objeto_serializable, lo normal, salvo que usemos Carga dinámica de clases, es que en ambos lados (el que envía y el que recibe la clase), tengan su propia copia del fichero objeto_serializable.class. Es posible que en distintas versiones de nuestro programa la clase objeto_serializable cambie, de forma que es posible que un lado tenga una versión más antigua que en el otro lado. Si sucede esto, la reconstrucción de la clase en el lado que recibe es imposible.

Para evitar este problema, se aconseja que la clase objeto_serializable tenga un atributo privado de esta forma

 private static final long serialVersionUID = 8799656478674716638L;

de forma que el numerito que ponemos al final debe ser distinto para cada versión de compilado que tengamos.

De esta forma, java es capaz de detectar rápidamente que las versiones de objeto_serializable.class en ambos lados son distintas.

Algunos entornos de desarrollo, como eclipse, dan un warning si una clase que implementa Serializable (o hereda de una clase que a su vez implementa Serializable) no tiene definido este campo. Es más, puede generarlo automáticamente, número incluido, si se lo pedimos. En eclipse basta con hacer click con el ratón sobre el símbolo de warning para que nos de las posibles soluciones al warning. Una de ellas genera el número automáticamente.

Escribir tipos básicos

package PkgEscribirFichData;

import java.io.*;

public class EscribirFichData {
  public static void main(String[] args) throws IOException {
  File fichero = new File("C:\\ADA\\FichData.dat");
  FileOutputStream fileout = new FileOutputStream(fichero);
  DataOutputStream dataOS = new DataOutputStream(fileout);
  String[] nombres= {"Juan","Pedro","Perico","Andrés"};
  int[] edades= {21,22,23,24};
  for (int i = 0; i < edades.length; i++) {
    dataOS.writeUTF(nombres[i]);
    dataOS.writeInt(edades[i]);
  }
  dataOS.close();	
  leerFichData(fichero);
}
  public static void leerFichData(File fichero) throws IOException {
    FileInputStream filein = new FileInputStream(fichero);
    DataInputStream dataIS = new DataInputStream(filein);
    String n;
    int e;
    try {
      while (true) {
        n = dataIS.readUTF();
        e = dataIS.readInt();
        System.out.println("Nombre: "+n+" Edad: "+e);
      
      }
    }catch (EOFException error){
      //System.out.println(error.getMessage());
    }
    dataIS.close();
  }
}

Escribir y leer objetos de un fichero

package PkgEscribirFichObject;
import java.io.*;

public class EscribirFichObject {
  public static void main(String[] args) throws IOException{
  Persona persona;
  File fichero = new File("C:\\ADA\\FichPersona.dat");
  FileOutputStream fileout = new FileOutputStream(fichero);
  ObjectOutputStream dataOS= new ObjectOutputStream(fileout);
  String[] nombres= {"Juan","Pedro","Perico","Andrés"};
  int[] edades= {21,22,23,24};
  for (int i = 0; i < edades.length; i++) {
    persona = new Persona(nombres[i],edades[i]);
    dataOS.writeObject(persona);
  }
  dataOS.close();
  leerFichObject(fichero);
  }
  
  public static void leerFichObject (File fichero) 
    throws IOException {
    Persona persona;
    FileInputStream filein = new FileInputStream(fichero);
    ObjectInputStream dataIS = new ObjectInputStream(filein);
    try {
      while (true) {
        persona = (Persona) dataIS.readObject();
        System.out.println("Nombre: "+persona.getNombre()+
            " Edad: "+persona.getEdad());
      }
    } catch (EOFException error) {
      //nada
    } catch (ClassNotFoundException error) {
      error.printStackTrace();
      System.out.println(error.getMessage());
    }
    dataIS.close();
    
  }
  
}
package PkgEscribirFichObject;
import java.io.Serializable;

public class Persona implements Serializable{
  private String nombre;
  private int edad;
  public Persona(String nombre, int edad) {
      this.nombre = nombre;
      this.edad= edad;
  }
  public Persona() {
      this.nombre=null;
  }

  public String getNombre() {
    return nombre;
  }
  public void setNombre(String nombre) {
    this.nombre = nombre;
  }
  public int getEdad() {
    return edad;
  }
  public void setEdad(int edad) {
    this.edad = edad;
  }
}

Actividad 8: Objetos Serializables

  • 8.1 Aplicación Spring Tool
  • 8.2 Aplicación JavaFX con IntelliJ