sábado, 24 de marzo de 2012

Serialización de objetos en Java II

votar
   En la entrada anterior vimos la forma de serializar objetos por defecto, es decir, utilizando los métodos writeObject() y readObject()  tal cual.

   Pero, ¿qué ocurre si los métodos así implementados no cubren nuestras necesidades? Por ejemplo, supongamos que tenemos un campo declarado transient pero que aún así necesitamos serializar, o unos datos que sólo queremos en el servidor receptor, y no en el emisor, como por ejemplo la actualización automática de la hora. 

   Para esto bastará con que escribamos nuestros propios métodos writeObject() y readObject() así:

   private void writeObject(ObjectOuputStream os){
      //aquí el código que necesitemos
   }
   private void readObject(ObjectInputStream is){
      //aquí el código que necesitemos
   }

   Estos métodos "tuneados" van dentro de la clase que implementa Serializable,  y una vez que los llamemos desde el método main() completarán la serialización según nuestras necesidades. Aquí tenéis un ejemplo:


import java.io.*;
import java.util.*;
public class TestSer{
   public static void main(String[] args) throws Exception{
      FileOutputStream fos = new FileOutputStream("test.ser");
      ObjectOutputStream os = new ObjectOutputStream(fos);
      Prueba p = new Prueba(358);
      os.writeObject(p);
      os.close(); 

      FileInputStream fis = new FileInputStream("test.ser");
      ObjectInputStream is = new ObjectInputStream(fis);
      p = (Prueba) is.readObject();

      System.out.println("Datos: " + p.getDatos());
      System.out.println("Hora actual: " + p.getHoraActual());
      is.close();
   }
}

class Prueba implements Serializable{
   private static final long seriaVersionUID = 3L; //luego explico esto
   private transient int datos; //son transient pero necesitamos serializarlos
   private transient Date horaActual;//lo queremos sólo en el servidor receptor

   public Prueba(int datos){
      this.datos = datos;
      this.horaActual = new Date();
   }

   private void writeObject(ObjectOutputStream os){
      try{
         os.defaultWriteObject(); //el método por defecto
         /*ahora escribimos lo que queremos que haga*/
         os.writeInt(datos); 
         System.out.println("Serialización");
      }catch(IOException e){
         e.printStackTrace();
      }
   }

   private void readObject(ObjectInputStream is){
      try{
         is.defaultReadObject();
         datos = is.readInt();
         horaActual = new Date();
         System.out.println("Recuperación");
      }catch(IOException e){
         e.printStackTrace();
      }catch(ClassNotFoundException e){
         e.printStackTrace();
      }
   }

   public int getDatos(){
      return datos;
   }

   public Date getHoraActual(){
      return horaActual;
   }
}

   Voy a explicaros ahora qué es eso de serialVerionUID. Ocurre que normalmente la serialización sucede en una JVM, y la reconstrucción en otra. Pero la JVM le asigna un número de versión, el serialVersionUID, a la clase serializable. Ahora bien, si más adelante se añaden o modifican campos en un objeto ya serializado, ese objeto no se podrá reconstruir porque el número de versión de la clase emisora no coincidirá con el de la clase receptora. Por eso se recomienda escribir el propio número de versión declarando un campo llamado serialVersionUID, que debe ser static, final y de tipo long, preferentemente private.

   private static final long serialVersionUID = 17L;

   Veamos por último cómo afecta la herencia a la serialización. Ya hemos visto que si una superclase implementa Serializable, entonces todas sus subclases la implementarán también automáticamente. Pero, ¿qué ocurre si una subclase es serializable pero su superclase no lo es?.

   En la entrada anterior veíamos que las variables serializadas mantenían el valor que tuvieran antes de la serialización al ser recuperadas. Por el contrario, las marcadas como transient reciben el valor por defecto tras la serialización, es decir, null en el caso de referencias a objetos y 0 en el caso de tipos primitivos. Sin embargo, todas las variables heredadas de la superclase serán reconstruidas con los valores que tenían inicialmente asignados, y no los que tenían en el momento de la serialización. Esto ocurre porque el constructor de la superclase, al contrario de lo que pasa con el constructor de la clase serializable, se ejecutará. Veamos un ejemplo:



import java.io.*;
public class SuperClase{
   public static void main(String... args){
      Empleado emp = new Empleado(1000, "Reponedor");
      System.out.println("Antes de la serialización. "+ emp.categoría + ": " + emp.sueldo +"€");
      try{
         FileOutputStream fos = new FileOutputStream("archivo.ser");
         ObjectOutputStream os = new ObjectOutputStream(fos);
         os.writeObject(emp);
         os.close();
      }catch(Exception e){  //simplificamos para no alargar el ejemplo
         e.printStackTrace();
      }
      try{
         FileInputStream fis = new FileInputStream("archivo.ser");
         ObjectInputStream ois = new ObjectInputStream(fis);
         emp = (Empleado) ois.readObject();
         ois.close();
      }catch(Exception e){
         e.printStackTrace();
      }
      System.out.println("Después de la serialización. " + emp.categoría + ": " + emp.sueldo +"€");
   }
}

class Empleado extends Empresa implements Serializable{
   String categoría;
   Empleado(int s, String c){
      sueldo = s; //esto se hereda de la superclase
      categoría = c; 
   }
}

class Empresa{ //esta clase no es serializable
   int sueldo = 800;
}

Este programa produce la salida:
Antes de la serialización. Reponedor: 1000€
Después de la serialización. Repondedor: 800€


Como veis, hay que tener cuidado con esto, porque aunque los dos sean sueldos de miseria, no es lo mismo cobrar 1000€ que 800€. : )

viernes, 16 de marzo de 2012

Serialización de objetos en Java I

votar
   La serialización consiste en convertir un objeto en una secuencia de bytes para guardarlo en un archivo o enviarlo por la red, y luego reconstruirlo, con los valores que tenía al ser serializado, para su posterior utilización. La serialización es muy utilizada en las bases de datos relacionales, pero tiene también otras aplicaciones.

   En Java, esta capacidad de serialización, es decir, de guardar información sobre un objeto para luego recuperarla, se llama persistencia.

   Para que un objeto sea serializable basta con que la clase a la que pertenezca, o una superclase de ésta, implemente la interfaz Serializable o su subinterfaz Externalizable, ambas en el paquete java.io.

  Muchas clases en la Java API implementan Serializable: clases de utilidad, como java.util.Date, o todas las clases de componentes de Swing GUI; pero por si acaso, asegúrate siempre, porque si intentas serializar un objeto de una clase que no implementa la interfaz Serializable, se producirá una NotSerializableException al ejecutar el programa.

    Implementar Serializable es muy sencillo, porque no tiene métodos. En realidad actúa como un marcador que indica que los objetos de una clase (o de sus subclases) son serializables, de esta manera: 

     class MiClase implements Serializable

   Ahora bien, para utilizar correctamente Serializable, sin liarla, hay que tener en cuenta algunos detalles.

   Primero, lo que estamos serializando son objetos y sus campos, así que las variables marcadas como static, es decir, que pertenecen a la clase y no al objeto, no pueden ser serializadas.

   Segundo, supongamos que queremos serializar un objeto que contiene una referencia a una instancia de una clase que no es serializable. Esto produciría la ya conocida NotSerializableException. Para evitarlo, debemos marcar esa instancia como transient.
   Todos los campos marcados como transient serán ignorados por la JVM en el proceso de serialización.

   Veamos todo esto con un ejemplo:

   public class MiClase implements Serializable{
      private int variable1;
      private static double variable2;
      private transient OtraClase oc = new OtraClase();
   }
   class OtraClase{} //OtraClase no es serializable

   Cuando un objeto de la clase MiClase sea serializado, sólo su variable "variable1" será serializada. La variable "variable2" no, porque es static y por tanto pertenece a la clase y no al objeto, y la variable oc tampoco porque está marcada transient. Si no añadiéramos el modificador transient tendríamos una NotSerializableException, pero también podemos utilizarlo cuando no nos interese serializar algo.

   Pasemos a ver ahora un ejemplo práctico de serialización y recuperación de objetos. Vamos a escribir unos datos en un archivo, serializarlos y recuperarlos después.

import java.io.*;
public class TestAgenda{
   public static void main(String[] args){
      Agenda a1 = new Agenda("Ana", "Martínez", "Fernández");
      Agenda a2 = new Agenda("Ernesto", "García", "Pérez");
      try{
         FileOutputStream fs = new FileOutputStream("agenda.ser");//Creamos el archivo
         ObjectOutputStream os = new ObjectOutputStream(fs);//Esta clase tiene el método writeObject() que necesitamos
         os.writeObject(a1);//El método writeObject() serializa el objeto y lo escribe en el archivo
         os.writeObject(a2);
         os.close();//Hay que cerrar siempre el archivo
      }catch(FileNotFoundException e){
         e.printStackTrace();
      }catch(IOException e){
         e.printStackTrace();
      }
      try{
         FileInputStream fis = new FileInputStream("agenda.ser");
         ObjectInputStream ois = new ObjectInputStream(fis);
         a1 = (Agenda) ois.readObject();//El método readObject() recupera el objeto
         a2 = (Agenda) ois.readObject();
         ois.close();
      }catch(FileNotFoundException e){
         e.printStackTrace();
      }catch(IOException e){
         e.printStackTrace();
      }catch(ClassNotFoundException e){
         e.printStackTrace();
      }
      
      System.out.println(a1);
      System.out.println(a2);
   }
}

class Agenda implements Serializable{
   private String nombre;
   private String p_Apellido;
   private String s_Apellido;
    /*getters y setters*/
   public String getNombre(){
      return nombre;
   }
   public void setNombre(String nombre){
      this.nombre = nombre;
   }
   public String getP_Apellido(){
      return p_Apellido;
   }
   public void setP_Apellido(String p_Apellido){
      this.p_Apellido = p_Apellido;
   }
   public String getS_Apellido(){
      return s_Apellido;
   }
   public void setS_Apellido(String s_Apellido){
      this.s_Apellido = s_Apellido;
   }
   public Agenda(String nombre, String p_Apellido, String s_Apellido){
      super();
      this.nombre = nombre;
      this.p_Apellido = p_Apellido;
      this.s_Apellido = s_Apellido;
   }
   public String toString(){
         return( getNombre() + " " + getP_Apellido() + " " + getS_Apellido());
      }
}
   
   Lo visto hasta ahora es la serialización por defecto. En la próxima entrada veremos como implementarla según nuestras necesidades. : )

sábado, 10 de marzo de 2012

String, StringBuffer y StringBuilder

votar
String, StringBuffer y StringBuilder son de las clases más utilizadas en programación Java, porque representan cadenas de caracteres y es difícil encontrar programas que no usen estas cadenas. En esta entrada vamos a explicar cuáles son las diferencias entre ellas, o más bien entre String y las otras dos, porque entre StringBuffer y StringBuilder sólo existe una diferencia, como veremos luego.

   Comencemos por la clase String. Los objetos de esta clase son inmutables, lo que significa que una vez creados no vas a poder modificarlos. Por ejemplo, si escribes

   String str = new String("Hola mundo"); //que originales que somos

ese "Hola mundo" queda "archivado" con str referenciándolo. Pero entonces, pensarás, si quieres añadirle algo a ese texto, ¿no puedes?. Sí, claro que puedes, pero no estarás modificando ese String, si no creando otro nuevo, por ejemplo

   str += " y parte del extranjero";

o utilizando el método .concat()

   str = str.concat(" y parte del extranjero");

te daría "Hola mundo y parte del extranjero", pero str ya no referenciaría a "Hola mundo", que quedaría "archivado", sin nada que lo referenciase, porque str ahora referenciaría al nuevo String.

   Veamos otro caso:

   String s1 = new String("abc");
   String s2 = new String("abc");

   ¿Cuántos objetos se han creado?. ¿Uno, dos?. En realidad, hay dos objetos de clase String creados por "new", pero un solo literal "abc". Para ahorrar memoria, los literales no se repiten, quedan "archivado" para futuros usos. Así que, aunque s1 y s2 no son iguales, porque son dos objetos distintos, si en lugar de usar new utilizamos sólo literales:

   String s1 = "abc";
   String s2 = "abc";

en este caso s1 == s2 porque referencian al mismo espacio en la memoria.

   Por último, si hacemos

   String s1 = "Hola mundo";
   String s2 = s1;
   s1 = s1.concat(" y parte del extranjero");

¿a qué se está refiriendo ahora s2?. Pues a "Hola mundo", porque s2 = s1 significa que los dos "apuntan" (no os riais los de C) a un mismo lugar en la memoria, y el nuevo s1 referencia ahora a otro lugar, con un nuevo String, pero s2 sigue refiriéndose al viejo, inmutable, "Hola mundo".

   Precisamente para asegurar la inmutabilidad de sus objetos, la clase String está marcada como final: sus métodos no pueden sobreescribirse.

   Llegamos así a la necesidad de las clases StringBuilder y StringBuffer: si tienes que hacer muchas modificaciones en tus objetos String, acabas con un montón de objetos inútiles ocupando espacio en la memoria. En cambio, los objetos de tipo StringBuffer y StringBuilder sí que pueden ser modificados.

   ¿Cuál es la diferencia entre los dos?. Bueno, los métodos de StringBuffer están sincronizados, y los de StringBuilder no. Eso significa que StringBuilder es más rápido, pero que si trabajas con programación multihebra es mejor que uses StringBuffer. Por lo demás son exactamente iguales y tienen la misma API.

   Veamos su diferencia con la clase String con un ejemplo:

   String s = "Hola mundo";
   s.concat(" y parte del extranjero";
   System.out.println(s);

   ¿Cuál será la salida?. ¡Pues "Hola mundo", porque no hemos asignado el nuevo String a s así:

   s = s.concat(" y parte del extranjero");

   Sin embargo, con StringBuffer: 

   StringBuffer sb = new StringBuffer( "Hola mundo");
   sb.append(" y parte del extranjero");//append hace lo mismo que concat
   System.out.println("sb: " + sb);

   La salida es "Hola mundo y parte del extranjero", porque hemos modificado sb.

   A continuación os dejamos un programita para mostrar algunos de los usos y métodos de estas clases. Consiste en averiguar si un String es palíndromo o no, es decir, si se puede leer igual hacia delante que hacia atrás. Para simplificarlo, hemos pasado de las tildes, pero podéis modificarlo a vuestro gusto.
import javax.swing.JOptionPane;
public class Palindromo{
   public static void main(String[] args){
      String s = JOptionPane.showInputDialog("Introduce una frase o palabra: ");
      String output = " ";
      s = s.replace(" ",""); //quitamos los espacios en blanco
      StringBuffer sb = new StringBuffer(s); //pasamos s como argumento a StringBuffer
      sb = sb.reverse(); //invertimos el orden de las letras
      if(s.equalsIgnoreCase(sb.toString())){ //comparamos sin importar si hay mayúsculas
         
         output = "Es un Palíndromo";
      }else{
         
         output = "No es un Palíndromo";
      }
      JOptionPane.showMessageDialog(null, output);
   }
}
   Si deseáis más información sobre estas clases y sus métodos, podéis acudir a los docs de Oracle
   Y como siempre, si tenéis alguna consulta, ya sabéis dónde estamos.; )

sábado, 3 de marzo de 2012

Aseveraciones (Assertions) en Java

votar
   Hace algunas entradas, estuvimos viendo el manejo de excepciones en Java. Hoy vamos a tratar de un tema relacionado: el uso de assert, o aseveraciones, en Java.

   Las aseveraciones te permiten comprobar las presunciones que haces sobre tu código mientras lo estás desarrollando, sin tener que manejar excepciones que tú crees que nunca, nunca, ocurrirán.

   Supongamos que escribes un código según el cual si x>0, sucede una cosa, y si x==0, sucede otra. Podríamos expresarlo así:

if(x>0){
  //código
}else{
  //más código
}
Tú supones que nunca, jamás de los jamases x<0; pero, ¿y si algo va mal y resulta que x tiene un valor negativo?. En este caso, se ejecutaría la parte de código correspondiente a else, como si x==0, con resultados impredecibles.
Para evitarlo, podemos usar una aseveración:


if(x>0)
  //código
}else{
  assert(x==0);
   //Ejecutar este código a menos que x sea negativo
}

De esta forma, si tu aseveración es cierta, el código continua ejecutándose normalmente. Pero si resulta ser falsa, se lanza un AssertionError que para el programa. Este AssertionError nunca debe manejarse.

A pesar de su utilidad durante el proceso de depuración, las aseveraciones no son tan utiliazadas por los desarrolladores como debieran. Por supuesto, esto no significa que debas lanzarte a escribir aseveraciones en tu código a diestro y siniestro. Deben ser usadas sobre todo para comprobar errores de lógica, y siempre en bloques de código sobre los que tengas un control total.

Esto es así porque las aseveraciones pueden ser habilitadas o inhabilitadas durante la ejecución. Si escribes una aseveración en un método público, ese método puede ser utilizado más adelante en condiciones desconocidas para ti. Por este mismo motivo, una aseveración nunca, nunca, nunca, puede afectar al desarrollo de un programa, porque imagínate qué ocurriría si del resultado de una aseveración dependiera una acción, pero quien utiliza el programa tiene inhabilitadas las aseveraciones.

Normalmente, las aseveraciones se habilitan mientras se depura el código, y después se vuelven a inhabilitar, y para el JVM es como si no existiesen. Pero si alguna vez el programa o la aplicación se comportan de forma extraña, pueden volver a habilitarse para ver qué es lo que ocurre.


Hay dos tipos de aseveraciones:
  • assert expresión;
  • assert expresión1: expresión2;
La única diferencia es que en el segundo tipo, además de lanzar un AssertionError, puedes añadir una frase que especifique el error. Por ejemplo, en el caso anterior podríamos haber escrito:

assert(x==0): "x es negativo";

Para habilitar o inhabilitar las aseveraciones se utiliza la línea de comandos:
  • java -ea o java -enableassertions para habilitarlas
  • java -da o java -disableassertions para inhabilitarlas
Si no se le añaden argumentos, se aplican a todas las aseveraciones de todas las clases, pero se puede especificar, de forma que sólo afecten a un paquete, a una clase... Por ejemplo:

java -ea:com.ejemplo.Test sólo habilita a las de la clase Test del paquete ejemplo.
java -ea -da:com.ejemplo.Test habilita a todas excepto a las de la clase Test del paquete ejemplo.
java -ea -da:com.ejemplo...habilita a todas excepto a las del paquete ejemplo y sus subpaquetes.

Por último, una advertencia, assert sólo está disponible a partir de Java 1.4 Si tu versión es más antigua, ya estás tardando en actualizarte.(¿Cómo puedes sobrevivir en la prehistoria?).

Para más información sobre el tema, podéis ir a docs.oracle.com o leer los libros Just Java de Joshua Linden y SCJP Sun Certified Programmer for Java 6 de Kathy Sierra y Bert Bates.