11 de febrero de 2019

Persistencia de Datos - Serialización Simple

Hasta ahora hemos visto objetos almacenados en memoria. Los instanciábamos directamente desde nuestro código fuente para usar objetos en nuestra aplicación. Está claro que cuando pasemos a aplicaciones reales no tiene sentido que los datos deban iniciarse "hardcodeados" y cada vez que haya que cambiarlos haya que editar el código fuente y recompilar.

Tampoco esperamos que siempre que arranque la aplicación tenga los mismos datos. Empezamos el curso hablando del concepto CRUD (crear, leer, actualizar y borrar). Si ejecuto mi aplicación en una sesión espero que los datos permanezcan en el estado en que la termine, no que vuelva al estado de cuando se compiló, perdiendo todo el trabajo.

En definitiva, necesitamos que los objetos se creen leyéndose desde un soporte en el que estén guardados, dándoles persistencia cuando se cierra nuestra aplicación y se libera la memoria

Necesitamos guardar nuestros datos en un soporte que nos permita conservarlos entre distintas ejecuciones de nuestra aplicación


Hay varias formas de hacerlo:
  • Almacenar en ficheros en disco (es lo que vamos a ver)
  • Guardar en una base de datos
  • Enviar por red

Serializar y Deserializar

Obviando por ahora las dos últimas opciones, para guardar en un fichero debemos transformar un objeto de cualquier complejidad a ceros y unos que será lo que se va a guardar en disco. Para ello necesitamos reducir todo a una "fila" de ceros y unos, ponerlos en serie: Serializar.

Cuando queramos leerlos debemos de hacer lo inverso, leer del fichero e ir leyendo las partes que nos permitan instanciar los objetos en memoria: Deserializar.

Serializar pasa objetos a un formato sencillo de guardar o transportar y Deserializar los reconstruye en sentido inverso


Aparte de serializar y deserializar, otra palabra que debemos incorporar a nuestro diccionario es parsear. Parsear es transformar un tipo en otro como cuando por ejemplo transformamos la String "5" en el int 5 mediante Integer.parseInt("5"). Esta transformación más o menos "sencilla" puede ampliarse a estructuras más complejas que una vez analizas producen el resultado que esperamos.

Si los datos leidos no tienen la estructura bien formada, tanto el parse como el deserializado no podrán producir el objeto y desencadenará habitualmente una excepción.

¿Cómo serializar?

Te invito a que inventes por tu cuenta una forma en que podrías guardar los datos por ejemplo de nuestros objetos Coche. En general si es tu primera vez seguramente pienses en algo como una línea con los valores separados por comas:

Peugeot 3008;Negro;4

Esto podría ser modelo, color y numeroDeRuedas de uno de nuestros objetos Coche. Añadiendo más líneas tendríamos en un fichero todos los que quisieramos. También podríamos poner una primera línea de encabezado para saber qué es cada valor.

La otra forma rápida que se te puede haber ocurrido es haciendo pares clave-valor:

Modelo=Peugeot 3008
Color=Negro
numeroRuedas=4

Si es así, enhorabuena, prácticamente has dicho la base de las formas más populares de hacerlo y que usan muchos servicios como estándares y las dos son texto plano: XML y JSON.

Hay más formas de serializar, como en binario, pero vamos a centrarnos en hacerlo en texto plano.

Formatos en texto plano

Vamos a ver una manera rápida de cómo serializar en CSV (Comma-Separated Values). Son ficheros normalmente con esa extensión de archivo y que guardan valores separados por un carácter especial (habitualmente ';')

Seguirían el patrón del primer ejemplo que pusimos, es muy sencillo y todavía se usan.

También nos los podemos encontrar con los valores ocupando un ancho fijo rellenando el espacio sobrante con espacios, en vez de separados por un carácter. Digamos que al modelo le podríamos dar 20 espacios, al color 10 y al número de ruedas 1.

Como veréis son formatos muy fáciles de serializar (en nuestro caso pasar a texto CSV) y deserializar (reconstruir un objeto desde un texto CSV). Usamos el siguiente código para ver un ejemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  public static String serializar (Coche coche) {
    return coche.modelo + ";" + coche.getColor() +
            ";" + coche.getNumeroDeRuedas() + ";" + coche.matricula;
  }

  public static Coche deserializar (String vehiculoCSV) {
    String[] campos = vehiculoCSV.split(";");
    Coche c = new Coche(campos[0], campos[1]);
    c.numeroDeRuedas = Integer.parseInt(campos[2]);
    c.setMatricula(campos[3]);
    
    return c;
  }

Y lo ejecutamos desde nuestro main:

1
2
3
4
  String coche1EnCsv = serializar(coche1);
  System.out.println(coche1EnCsv);
  Coche coche4 = deserializar(coche1EnCsv);
  System.out.println(coche4);

Esto nos dará la siguiente salida (serializar:cada campo separado por ";" y deserializar: dando un objeto igual al que se serializó):

Seat Ibiza;Rojo;4;1234 BBB
Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas

Esta sencillez acarréa una serie de problemas:
  • Si usamos ancho fijo:
    • ¿Qué pasa cuando el tamaño marcado para el campo sea más grande? se truncará y no tendremos el valor completo.
  • Si usamos CSV
    • ¿Qué pasa cuando no tengamos unos valores fijos?
    • ¿Y si un campo contiene el caracter separador?
    • ¿Y si un valor es una colección? No podremos hacerla crecer hasta donde queramos
    • Si no tengo valor para un campo debo seguir poniendo ';' para que siga corriendo la posición al siguiente valor.
Independientemente de soluciones "mas o menos caseras", tenemos un problema en el horizonte y eso será un problema de mantenimiento y/o interoperabilidad de sistemas.

Por esas limitaciones existen formatos que nos permiten organizar esas estructuras más complejas. Los más habituales son XML y JSON que mejoran la interoperabilidad con otros sistemas

¿Qué tengo que serializar?

Lo primero que tengo que pensar es qué debo guardar y qué no. No se trata de dar persistencia a cada campo. Si vamos a usar serialización de Java debemos echar un vistazo a la interfaz Serializable.

Serializable es una interfaz muy fácil de implementar, simplemente se define que la clase lo implementa y sólo debemos añadir un número de versión (serialVersionUID) aunque si no lo ponemos nos lo marcará como un warning, no un error. Este número sirve para saber si es compatible el dato serializado con el objeto que se quiere obtener. No prestar atención a este número puede ser un quebradero de cabeza con nuevas versiones.

Aquí no vamos a ver esta interfaz, no vamos a usar la serialización nativa de Java, nos centraremos en guardar datos en JSON y lo veremos en futuras sesiones pues es el formato de notación nativa de JavaScript y nos va a servir también para trabajar fácilmente con datos en aplicaciones web.

Lo que vamos a ver para terminar esta sesión es cómo preparo mi clase para indicar qué se debe serializar y qué debemos tener en cuenta para que nos funcionen otras librerias de serializado como la que vamos a usar para JSON (jsonbeans de EsotericSoftware).

Normalmente se va a serializar:
  1. El nombre de la clase cuando haga falta.
  2. Los campos NO marcados como transient (esta palabra clave indica que un campo no forma parte de su estado persistente, normalmente son campos calculados: la edad si tengo la fecha de nacimiento o un mapa para mejorar el acceso a valores por clave, esos campos los puedo calcular al inicializar el objeto).
    Este modificador es usado por librerías que serializan en función de este modificador (como Serializable o jsonbeans). Sin embargo no hay que confundirlo con nuestro ejemplo sobre CSV en el que se ha implementado sin tener en cuenta el modificador transient y evidentemente no afecta a la serialización
  3. Los campos cuyo valor sea distinto al que reciben al inicializar la variable.
  4. Se serializarán los objetos internos conforme a su tipo: si modelo fuera un tipo que contuviese una un objeto de tipo Marca, se serializaría modelo como parte de Coche y marca como parte de Modelo, etc... es decir que se sigue profundizando hasta que todo lo que se debe serializar está reducido a texto plano que se pueda deserializar.
No obstante, todo esto depende de la implementación concreta de la librería que usemos y de cómo guardemos los datos, pero sería una lista de "sospechosos habituales" (por ejemplo algunas formas de serializar podrían sólo guardar los campos public).

Como verás no se guardan las variables de clase. Es normal teniendo en cuenta que normalmente son constantes y están definidos sus valores en la clase.

No debemos olvidar que lo normal es que para reconstruir el objeto se utilice la información de su clase (Class) para crear una nueva instancia e ir asignándole los valores a cada campo guardado. A menos que se haga un serializador personalizado, normalmente se usará el constructor vacío para instanciarlo, así que debemos asegurarnos que nuestros tipos serializables lo tengan.

¿Cómo preparo mi clase para que se serialice correctamente?

Básicamente debes hacer lo siguiente:
  1. Usar transient en los campos que no quiero serializar
  2. Asegurarme de tener un constructor vacío
  3. Que los otros tipos que se deben serializar como parte de mi clase también cumplen estas normas
Vamos a ver un ejemplo usando JSON en la siguiente sesión.

8 de febrero de 2019

Leer y guardar texto en un fichero

En esta sesión voy a poner sendos ejemplos de lectura y escritura de un texto en disco.

Si buscáis por Internet encontraréis muchas formas distintas de hacerlo usando distintos tipos. Esto es debido a que hay distintas implementaciones de las clases Reader y Writer:
  • Unas permiten especificar el soporte (no tiene por qué usarse un fichero, podríamos leer y escribir de un socket web o un buffer de datos cualqueira)
  • Unas mejoran el rendimiento del ancho del buffer
  • Unas están especializadas en guardar texto y otras para datos en crudo (raw)
  • Algunos tienen métodos particulares que nos pueden resultar últiles y no pertenecen a las interfaces (como newLine())
  • Además hay que sumar las distintas librerías que hay para facilitar el tratamiento de estas operaciones como Apache common o guava.

Por todo lo anterior y desde mi punto de vista, voy a usar la forma más eficiente y flexible para hacer operaciones con texto sobre ficheros:
  1. Usaré sólo Java Nativo.
  2. Usaré BufferedReader/BufferedWriter porque me va a encapsular un Reader/Writer para mejorar el rendimiento en los accesos al disco (en cada acceso va a usar el buffer completo y por tanto necesitará menos operaciones de acceso)
  3. Usaré un InputStreamReader/OutputStreamWriter porque me van a permitir especificar la codificación de mi texto (usaré UTF-8)
  4. Usaré un FileInputStream/FileOutputStream para realizar operaciones sobre archivos.
Este tipo de operaciones van a reservar recursos de mi sistema (si tengo un fichero abierto porque lo estoy usando, otro proceso no podrá usarlo, como cuando queremos sacar nuestro USB y nos dice que está en uso). Por tanto, cuando dejemos de usar un archivo hay que liberarlo para que no se quede reservado impidiendo su uso. Para eso existe el método close().

También estás operaciones suelen declarar varias excepciones (cuando no existe un fichero, no se tiene acceso, hay una codificación deconocida... cualquier error de entrada y salida). Esto hace que deban estar rodeadas de la cláusula try-catch para controlarlas.

No obstante, como BufferedReader/Writer implementan la interfaz AutoCloseable, voy a usar un try-with-resources para que se encarge automáticamente de cerrarme el fichero cuando acabe de usar mi Reader/Writer.

El código para la lectura queda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  public static String leer (String ruta) {
    String leido = "";
    
    try (BufferedReader buffer = new BufferedReader(
                    new InputStreamReader(
                            new FileInputStream(ruta),
                            "UTF-8"))) {
      String linea;
      while((linea = buffer.readLine()) != null) {
        leido += linea + System.lineSeparator(); //Esto variará
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
    
    return leido;
  }

Para la escritura:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  public static void escribir (String texto, String ruta) {
    
    try (BufferedWriter buffer = new BufferedWriter(
                    new OutputStreamWriter(
                            new FileOutputStream(ruta),
                            "UTF-8"))) {
      buffer.write(texto); //Esto variará
    } catch (Exception e) {
      e.printStackTrace();
    }
    
  }
Nota: Si queremos guardar en varias lineas es mejor usar el método buffer.newLine() antes que usar la secuencia de escape "\n", pues hará el salto de línea correcto independientemente del SO donde se esté ejecutando (no es igual para Linux (\r\n) que para Windows (\n))

He encapsulado el código en métodos estáticos para que quede más claro el código para leer y el código para escribir.

Con esta sintaxis tienes optimizada la escritura de texto en ficheros y puedes elegir la codificación


Para practicar con una ruta relativa con un directorio, nos tenemos que crear una nueva carpeta en nuestro proyecto (New > Folder) que llamaremos datos. Cuando ejecutemos el código de abajo y refresquemos el proyecto (Refresh F5), nuestro fichero aparecerá dentro de ella con el nombre miTexto.txt.

Vamos a ejecutarlo con un ejemplo sencillo en nuestro main:

1
2
3
4
5
6
7
8
  String ruta = "datos\\miTexto.txt";
  String texto = "Primera linea.\nSegunda Linea.";
  
  System.out.println("Guardo:\n" + texto);
  escribir(texto, ruta);
  
  String textoLeido = leer(ruta);
  System.out.println("\nLeido:\n" + textoLeido);

De esta forma vemos que nuestro texto se guardo en disco y se ha leido devolviendo el mismo resultado.

Evidentemente no todo será guardar una cadena de texto formada y devolver un texto leido, pero con este ejemplo se ve la estructura básica en un ejemplo sencillo. Luego tocará cambiar la linea marcada con el comentario "Esto variara" con lo que haya que hacer al leer cada línea o al tener que guardar varias lineas distintas por ejempo al guardar elementos de una colección.

La primera vez puede parecer complejo ver cómo se instancia nuestro buffer (es normal, le pasa a todo el mundo). Usándolo más veces al final todo tiene sentido, pero se soluciona fácilmente guardando el enlace donde se ve la sintaxis o simplemente haciéndonos nuestros propios métodos que encapsulen este código.

29 de enero de 2019

Polimorfismo

El Polimorfismo es uno de los conceptos que siempre suenan cuando se habla de POO junto con la encapsulación o la herencia. Sin embargo aplicarlo no es nada nuevo, llevamos ya muchas sesiones usándolo aunque no hayamos nombrado el concepto.

En resumen: dos objetos de distinto tipo pueden recibir el mismo mensaje (invocación de un método) y responder de dos (Poli) formas (morfia) distintas.

Polimorfismo: objetos de distinto tipo pueden recibir el mismo mensaje y responder de forma distinta


¿Dónde hemos usado polimorfismo?

Te invito a que pienses un momento sobre si hemos utilizado una misma invocación a distintos objetos y el resultado fue distinto.

Estos ejemplos ya vistos usan polimorfismo:
  1. Método toString() de todos los objetos que hemos visto (Integer, String, Coche, etc...) todos nos devuelven un String, pero cada uno lo construye de maneras distintas
  2. Particularmente para nuestros ejemplos de vehículos, unos tienen matrícula, otros ruedas, incluso un barco con su eslora... a todos se les llama con toString() y devuelven un String pero lo calculan de manera distinta
  3. Método setColor(String color): Ahora la invocación añade el argumento color. Podíamos poner el color que quisieramos hasta que instanciamos nuestra Harley-Davidson que sólo aceptaba dos colores. Pasó de una simple asignación a avisarnos que hay limitaciones con ese objeto = polimorfia.
  4. Veremos más ejemplos cuando lleguemos a las Interfaces viendo como tipos sin relación responden distinto a un mismo mensaje.

Esta es la entrada más corta que he hecho y quiero dejarla así porque la próxima vez que oigas el concepto polimorfismo no quiero que te eches las manos a la cabeza, quiero que pienses que es uno de los conceptos más simples pero potentes de la POO.

28 de enero de 2019

Clases anónimas

Terminábamos nuestra sesión de clases abstractas preguntándonos ¿por qué una clase abstracta (que no se puede instaciar) tiene constructores?

Obviando que los constructores declarados en VehiculoConRuedas sirven para aprovechar constructores de Vehiculo, lo correcto es que las clases abstractas contengan todo el código relacionado con sus miembros.

Es normal que el código que implementemos para inicializar los atributos modelo y color, pueda heredarlo para no tener que implementarlo en cada subtipo (mejorando el mantenimiento como ya hemos repetido varias veces)

No obstante, hay un caso muy importante para tener también constructores incluso en clases abstractas: instanciar con clases anónimas.

Una clase anónima nos va a permitir declarar una clase e instanciarla en la pequeña parte de código donde nos haga falta, haciendo nuestro código más legible y económico. Como podemos deducir de su definición, es una clase que no tiene nombre, no está contenida en su propio archivo .java, ni es interna (esto último todavía no lo hemos visto)

Una clase anónima nos va a permitir declarar una clase e instanciarla brevemente, haciendo nuestro código más legible y económico


Hasta ahora hemos visto que cada clase tenía que tener su propio archivo .java con el mismo nombre o Eclipse nos daba un error (MiClase tiene el código en el archivo MiClase.java).

Sin embargo, vamos a encontrarnos infinidad de situaciones en las que necesitaremos un objeto que podría usar un tipo prácticamente igual a otro ya creado, salvo por un pequeñísimo cambio que además sólo vamos a utilizar en ese objeto en un punto concreto de nuestro código y nunca más nos interesará.

Es demasiado repetitivo y excesivo tener que crear un archivo para una nueva clase que herede de un tipo sólo para instanciar un objeto en una línea de código (imaginad crear un botón en un menú donde lo único que cambia con otro botón es qué tiene que pasar al hacer click... ¿nos creamos una clase para cada botón?). Para estos casos, Java tiene las clases anónimas que son una herramienta poderosísima de personalización de un tipo.

Una clase anónima se crea al vuelo a la hora de instanciar un objeto que necesitamos y no encaja exactamente con ningún tipo. Se definie llamando al constructor de un tipo conocido y añadíendole un cuerpo con el comportamiento personalizado.

Vamos a ver un ejemplo con nuestro código sobre Vehiculo creándonos un triciclo. No nos vamos a crear una nueva clase Triciclo, pues sólo queremos imprimir nuestro objeto triciclo, pero no encaja con ningún tipo que tengamos: tiene tres ruedas (no es una Moto) y no tiene matrícula (no es un Coche).
NOTA: No es una buena práctica utilizar tipos que tengan todo lo que necesitamos y hacer null el resto de atributos. Los objetos deben ser del tipo con el que se construyen, el resto son apaños que aumentan la deuda técnica.

Lo primero es seleccionar la clase que más se acerca a lo que queremos: VehiculoConRuedas. Éste sería el código para crearme un triciclo:

1
2
3
4
5
6
7
8
  VehiculoConRuedas triciclo = new VehiculoConRuedas("Fisher-Price", "Multicolor") {
    
    @Override
    public int getNumeroDeRuedas() {
        return 3;
    }

  };

Vemos que declaramos una variable del tipo VehiculoConRuedas (pues hereda de éste tipo), pero al ser una clase abstracta y no poderse instanciar (está incompleta) directamente nos añadirá lo que le falta por implementar: getNumeroDeRuedas().

Si imprimimos nuestro triciclo veremos que el método toString() funciona igual que en las clases NO anónimas Coche y Moto que heredaban de VehiculoConRuedas. Tenemos la siguiente salida.

Fisher-Price (Multicolor), 3 ruedas

¿Sólo puedo modificar métodos que ya existan?

No. Al ser VehiculoConRuedas una clase abstracta debemos implementar los métodos no implementados, pero podemos añadir más miembros o modificar (incluso reutilizar) los que ya estén implementados.

En nuestros ejemplos anteriores nos hemos creado una Moto. Vamos a crearnos otra que sea una Harley-Davidson. Ésta marca de moto sólo permite dos colores: negro y rojo. Vamos a cambiar el método setColor(String) para que no acepte ningún color que no sea uno de esos dos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  Moto harley = new Moto("Harley-Davidson", "Rosa") {

    @Override
    public void setColor(String color) {
        if(!(color.equals("Rojo") || color.equals("Negro"))) {
            System.out.println("No se permite ese color para " + modelo);
        }
        else {
            super.setColor(color);
        }
    }
    
  };
  System.out.println(harley);

Con este código tenemos la salida:

No se permite ese color para Harley-Davidson
Moto: Harley-Davidson (null), 2 ruedas

Nos avisa que el color no está permitido y de hecho vemos que nos imprime null porque no se ha asignado. Si le asignamos (reutilizando con super el método que hereda) un color válido si que funcionará:

1
2
  harley.setColor("Negro");
  System.out.println(harley);

Moto: Harley-Davidson (Negro), 2 ruedas

¿Necesito una clase abstracta para crear una clase anónima?

No. Vamos a crearnos un barco usando la clase Vehiculo que no es abstracta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  Vehiculo barco = new Vehiculo("CMB Yachts", "Blanco") {
    double eslora = 47.8;

    private double getEslora() {
        return eslora;
    }
    
    @Override
    public String toString() {
        return super.toString() + " con " + getEslora() + "m de eslora";
    }
    
  };
  System.out.println(barco);

Vemos que podemos tanto sobrescribir, como crear nuevos miembros (incluyendo atributos como eslora) y no afecta si el tipo del que heredamos es abstracto o no. Incluso podemos crear clases anónimas usando una Interface como veremos cuando las conozcamos. El resultado por consola es:

CMB Yachts (Blanco) con 47.8m de eslora

Las clases anónimas y el concepto scope

Un aspecto importante a destacar es que las clases anónimas se crean en el ámbito (scope) de otra y tienen acceso a los miembros tanto de su clase como del tipo donde se crean incluso con el modificador de acceso private. Esto nos permite referenciar valores que permanecen privados pero que pueden ser usados en la implementación de nuestra clase anónima (por ejemplo un botón puede desencadenar un cambio en un componente privado de un formulario).

Las clases anónimas tienen acceso a todos sus miembros y a los del tipo donde se crean


Vamos a ver un ejemplo. Declaro un atributo privado:

1
  private static int miCodigoSecreto = 12345; // ¿En serio?

Pero si me instancio una clase anónima dentro de su ámbito voy a tener acceso:

1
2
3
4
5
6
7
8
9
  Coche cocheConPin = new Coche() {

    @Override
    public String toString() {
      return "El código secreto es: " + miCodigoSecreto;
    }
    
  };
  System.out.println(cocheConPin);

Si imprimo mi nuevo objeto tendré la salida:

El código secreto es: 12345

Siendo anónimas ¿Cómo nos referimos a su class?

Las clases van a tomar por defecto el nombre del tipo donde se instancian más un autonumérico precedido de un dolar ($). Podemos verlo con el siguiente código:

1
2
3
  System.out.println(triciclo.getClass());
  System.out.println(harley.getClass());
  System.out.println(barco.getClass());

Como vemos nos salen 3 clases distintas (instanciadas en la clase Igualdad) y lo único que cambie es el autonumérico:

class Igualdad$1
class Igualdad$2
class Igualdad$3

Conclusión

Puede que todavía no veas las posibilidades que aportan las clases anónimas, pero cuando conozcas los Listener, las lambdas, los stream, las Interfaces Funcionales y otras capacidades de Java, no querrás seguir programando sin ellas.

25 de enero de 2019

Clases abstractas

Las clases abstractas son clases pensadas para ser heredadas de ellas y que encapsulan código dirigido . Se comportan exactamente igual que las clases que hemos visto hasta ahora con estas diferencias:
  1. Debe declararse que son abstractas usando en su declaración la palabra reservada abstract como modificador.
  2. Tendrán métodos abstractos que se declararán también con abstract. Estos métodos no tienen cuerpo y serán implementados por sus subtipos.
  3. Al carecer de una implementación completa (los métodos abstractos no están implementados), no podrán construirse instancias de este tipo pues para poder crear objetos deben tener todos sus miembros implementados.
NOTA: Si bien el código no nos arrojará ningún error ni nos obligará a cumplir las condiciones 2 y 3, el hecho de no cumplirlas sería un indicativo de que no tiene sentido declararla abstracta.

Con las clases abstractas vamos a tener código relacionado en un único sitio para que por herencia lo completen y aprovechen sus subtipos


Como todo se ve mejor con un ejemplo, vamos a crearnos una clase abstracta llamada VehiculoConRuedas, que servirá para heredar de Vehiculo y gestionar lo que tenga que ver con ruedas. Vamos a crearla de la misma forma que lo hicimos la sesión anterior, pero en esta ocasión, aunque pensemos que debemos escoger el atributo numeroDeRuedas, no lo vamos a marcar y sólo le vamos a poner el nombre "VehiculoConRuedas" a la clase abstracta.
NOTA: Si ya tuviera más clases con comportamientos a agrupar (por ejemplo ya tuviera creada Moto), Eclipse nos permite añadir todas las clases de las que queramos extraer la superclase.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class VehiculoConRuedas extends Vehiculo {

  public VehiculoConRuedas() {
    super();
  }

  public VehiculoConRuedas(String modelo, String color) {
    super(modelo, color);
  }

}

Eclipse ha insertado en nuestra jerarquía la clase VehiculoConRuedas entre Coche y Vehiculo para que todo siga funcionando correctamente. Todo esto también podríamos haberlo escrito a mano y no debe dejar de practicarse para coger soltura.

Si abrimos nuestra nueva clase vemos que sólo tiene dos constructores y que es una clase normal. Vamos a añadirle un método abstracto para ver qué una clase normal no lo permite. Lo haremos simplemente declarándolo incluyendo el modificador abstract y en vez de un cuerpo terminaremos con punto y coma:

1
public abstract int getNumeroDeRuedas();

Tendremos un error en Eclipse que nos dirá que quitemos el modificador abstract del método o que hagamos la clase abstracta. Vamos a declarar nuestra nueva clase abstracta y ahora ya no tendremos errores.

Nuestra clase completa queda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public abstract class VehiculoConRuedas extends Vehiculo {

  public VehiculoConRuedas() {
    super();
  }

  public VehiculoConRuedas(String modelo, String color) {
    super(modelo, color);
  }

  public abstract int getNumeroDeRuedas();
}

Guardamos los cambios en VehiculoConRuedas y volvemos al código de Coche para ver que hereda de VehiculoConRuedas (lo hizo Eclipse) pero nos aparece un error: No tiene implementado el método getNumeroDeRuedas().

Podemos usar Eclipse para que nos añada este método vacío o hacerlo a mano. Si aceptamos la solución que nos ofrece Eclipse (Add unimplemented methods) cuando nos situamos encima del error vamos a obtener en Coche el siguiente código (seguramente estará añadido al final de todo):

1
2
3
4
5
  @Override
  public int getNumeroDeRuedas() {
    // TODO Auto-generated method stub
    return 0;
  }

Con esto Eclipse nos crea ese método vacío y nos añade una tarea (//TODO) para rellenar la implementación. Como nosotros tenemos precisamente un atributo llamado numeroDeRuedas vamos a devolver su valor como hacemos con cualquier otro getter:

1
2
3
4
  @Override
  public int getNumeroDeRuedas() {
    return numeroDeRuedas;
  }

Ahora si imprimimos un coche veremos que el método toString() que implementamos funciona correctamente:
  1. Obtiene el modelo y color desde Vehiculo
  2. Obtiene el número de ruedas usando u miembro heredado de VehiculoConRuedas
  3. El método toString() está implementado en Coche.
Compruébalo añadiendo este código:

1
  System.out.println(coche1);

A nuestro main hecho en la sesión de igualdad, verás que nos muestra por consola:

Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas

Reutilizamos nuestra jerarquía

Ahora llega el momento de reutilizar nuestra jerarquía creándonos una clase Moto. Podemos usar las ventajas de Eclipse y decir en la ventana de creación de clase que hereda de VehiculoConRuedas. Así tenemos ya trabajo hecho:

1
2
3
4
5
6
7
8
9
public class Moto extends VehiculoConRuedas {

  @Override
  public int getNumeroDeRuedas() {
    // TODO Auto-generated method stub
    return 0;
  }

}

Nos queda implementar el método abstracto getNumeroDeRuedas(). No es necesario crearse un atributo para completar este método. Bastaría con que siempre devuelva el valor 2 para ver el ejemplo:

1
2
3
4
  @Override
  public int getNumeroDeRuedas() {
    return 2;
  }

Colocando más cosas en su sitio

Si ahora hago la prueba de imprimir una nueva moto veré que obtengo un resultado bastante feo como ya vimos en la sesión sobre el método toString():

1
2
  System.out.println(new Moto());
  // Devuelve algo como Moto@52e922

Tiene sentido colocar el código que está relacionado en el mismo sitio: Si todos los Vehiculo tienen modelo y color, y todos los VehiculoConRuedas tienen ruedas, el método toString() podría ir mejorándose en función de qué nivel ocupa mi tipo en la jerarquía. Voy a implementarlo haciendo dos cosas:
  1. Crearé un constructor para Moto que acepte modelo y color. (puedes usar Source > Generate Constructors from Superclass
  2. Moveré la parte del código de Coche.toString() a las clases a las que pertenecen los miembros correspondientes.
Aquí están los cambios que hay que hacer:

En Vehiculo:

1
2
3
4
  @Override
  public String toString() {
    return modelo + " (" + getColor() + ")";
  }

En VehiculoConRuedas:

1
2
3
4
  @Override
  public String toString() {
    return super.toString() + ", " + getNumeroDeRuedas() + " ruedas";
  }

En Coche:

1
2
3
4
  @Override
  public String toString() {
    return "Placa " + matricula + " - " + super.toString();
  }

En Moto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Moto extends VehiculoConRuedas {

  public Moto(String modelo, String color) {
    super(modelo, color);
  }

  @Override
  public int getNumeroDeRuedas() {
    return 2;
  }

  @Override
  public String toString() {
    return "Moto: " + super.toString();
  }

}

Ahora si imprimo los ejemplos de toString() anteriores:

1
2
  System.out.println(coche1);
  System.out.println(new Moto("Suzuki", "Negro"));

Tengo la salida:

Placa 1234 BBB - Seat Ibiza (Rojo), 4 ruedas
Moto: Suzuki (Negro), 2 ruedas

Queda demostrado el ahorro de tiempo tanto construyendo como manteniendo nuestro código si nos apoyamos en jerarquías que me permitan tener el código relacionado escrito en un único sitio para que luego sea aprovechado de manera particular en otras clases más especializadas.

Si alguno ha examinado el código quizás pueda preguntarse: Si las clases abstractas no se pueden instanciar ¿Por qué VehiculoConRuedas tiene constructores?

Es una buena pregunta, vamos a verlo mejor en la sesión que explica qué son las clases anónimas.

24 de enero de 2019

Herencia

Ya he adelantado que las clases pueden heredar de otras clases. Esta capacidad de heredar nos va a servir para que clases relacionadas que deben tener miembros parecidos o repetidos no tengan que ser implementados para cada clase. Si una clase superior (superclase o clase padre) tiene una serie de miembros implementados, pueden ser aprovechados por clases que hereden de ella (subtipo o clase hija).

Con la herencia reutilizamos código de clases fuertemente relacionadas, estableciendo una relación "es un" o de especialización


Se van a heredar todos los miembros (no los constructores), pero su visibilidad lo regirá el control de acceso que marquemos (un miembro private no se podrá ver desde su heredero como ya vimos, deberá ser marcado mínimo como protected para tener acceso desde un heredero)

Para poder definir y usar la herencia vamos a usar las palabras reservadas extends y super:
  • extends: sirve para definir la clase de la que se hereda (superclase) en la propia cabecera de la clase que hereda (subtipo).
  • super: sirve para referirnos a miembros de la superclase (parecido a como haciamos con this)

Vamos a verlo con el código. En nuestro ejemplo vamos a crear una clase padre con nombre Vehiculo que nos sirva también para aeronaves, embarcaciones, etc... y decidimos que lo que deben tener todos los vehículos es un modelo y color. La forma de hacerlo es extraer los miembros que tienen que ver con modelo y color y moverlos a nuestra nueva clase Vehiculo.

Lo más fácil usando Eclipse es usar las ayudas que nos ofrece este IDE. Si hacemos click derecho sobre la Coche (en el Package Explorer o directamente en la ventana de su código fuente) elegimos Refactor > Extract Superclass... y en la ventana que nos aparece le ponemos el nombre Vehiculo y elegimos en la parte de abajo los miembros que vamos a extraer. Estos son: modelo, color, getColor(), setColor(String). Al clickar en Finish tendremos nuestra superclase creada (ver en su fichero .java).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Vehiculo {

    protected String modelo;
    String color;

    public Vehiculo() {
        super();
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

}

Ahora vemos que Coche carece de esos miembros (en su código ya no aparecen) y lo que debemos hacer es declarar que herede de Vehiculo con la palabra reservada extends. Eclipse ya ha hecho ese trabajo por nosotros. Si hacemos la refactorización a mano no nos podemos olvidar de declarar esta herencia:

1
2
3
4
5
public class Coche extends Vehiculo {

  // Codigo sin los miembros de Vehiculo

}

Ahora sí que son miembros que conoce la clase Coche. Y los podremos usar de la misma forma que ya los usabamos. Puedes comprobarlo con el código que hicimos en la sesión de igualdad de objetos.

De hecho vamos a cambiar un poco el código para ver cómo los valores de tipo Coche SON también un Vehiculo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Igualdad {

  public static void main(String[] args) {
    Coche coche1 = new Coche("Seat Ibiza", "Rojo");
    coche1.setMatricula("1234 BBB");
    //...
    Vehiculo coche3 = coche1;
    // coche3.setMatricula("5678 CCC");// error!

    //...
  }
}

Aquí vemos que coche1 puede asignarse a una variable de tipo Vehiculo y el resto de código funciona correctamente. Es una buena práctica acostumbrarse a usar los tipos más básicos de lo que necesito: como sólo uso los miembros de modelo y color, me sirve un tipo Vehiculo. Sin embargo si quisiera usar el método setMatricula(String) sí que tendría que usar una variable de tipo Coche, porque sólo así tendré acceso a ese método como ya os adelantaba en la sesión donde construiamos objetos. Queda demostrado entonces que las variable dan acceso a los miembros de su tipo, independientemente de que el valor sea de un subtipo más especializado.

Con la herencia se crea una jerarquía de tipos más especializados compatibles con sus ancestros


Aprovechando los constructores

Hemos dicho que se heredan los miembros pero no los constructores: ¿Cómo aprovecho el código que ya tenía en los constructores?

Lo que hacemos es acceder a miembros de las clases superiores o sus contructores. Voy a crearme un contructor de Vehiculo con los dos parámetros modelo y color:

1
2
3
4
public Vehiculo(String modelo, String color) {
    this.modelo = modelo;
    this.color = color;
}

Ahora puedo llamar al constructor de la superclase para aprovecharlo y sólo añadir código nuevo. En mi caso uso el constructor de Coche con modelo y color, pero voy a vaciarlo del código repetido ahora en Vehiculo. Nuestro constructor pasa de esto:

1
2
3
4
  public Coche (String color, String modelo) {
    super(color, modelo);
    numeroDeRuedas = 4;
  }
NOTA: Asigno 4 a numeroDeRuedas pues ya no utiliza el constructor por defecto Coche(). Te propongo que modifiques los otros contructores para que reutilicen éste como vimos en la sesión sobre constructores.

Con la palabra reservada super hacemos referencia a la superclase (igual que con this hacemos referencia a sí mismo). Si usamos el asistente de contenido veremos que nos salen los miembros de la superclase a los que tengo acceso: modelo, color, getColor(), setColor(String) y todos los de Object que es "el abuelo".

Con lo que hemos visto aquí ya tenemos nuestra primera jerarquía de clases. En la siguiente sesión sobre clases abstractas vamos a hacernos otra clase Moto que también herede de Vehiculo, pero en medio, para reutilizar el código que tenga que ver con el número de ruedas, crearemos la clase abstracta VehiculoConRuedas.

Sentencia switch

La sentencia switch nos va a servir para el control del flujo de nuestra aplicación.

Al contrario que if que ejecuta un bloque si se cumple una expresión (boolean) y si añadimos else se ejecuta otro bloque si no se cumple, con switch podemos tener varios caminos a seguir evaluando una expresión de otros tipos y delegar la ejecución sobre el caso que encaje, entre una variedad de ellos (no sólo true o false).

He puesto esta entrada en esta parte menos usada porque, según mi experiencia, realmente se usa poco. Esto tiene fácil explicación:
  1. Su sintaxis se suele olvidar por usarse poco, ser bastante larga y dar bastante pereza.
  2. Normalmente con unos pocos casos usando if/if-else conseguimos lo mismo con mayor rendimiento.
  3. Usar un Map para recuperar valores (resultados de un switch) por una clave (casos case) mejora el mantenimiento.
  4. Tiene limitaciones en los tipos que se pueden usar para los casos (básicamente int, String, y Enum) .
  5. Seguramente, conscientes de esto en Java 12 mejoran su sintaxis.
Pero para que no quede sin ver una sentencia que se puede encontrar en la mayoria de lenguajes de programación vamos a ver su sintaxis y un ejemplo.

La sentencia switch se define usando la palabra reservada switch, estableciendo un valor a evaluar (expresion) y varios apartados conteniendo cómo proceder en función del valor evaluado (case) que puede declarar al final qué hacer en caso de no encontrar coincidencias (caso default):

Nos hacemos el siguiente método:
static void testSwitch(int expresion) {
  switch (expresion) {
  case 1:
    System.out.println("Caso 1");
    break;
  case 2: case 3:
    System.out.println("Caso 2 o 3");
    //break; Si omitimos "break" sigue ejecutando
  case 4:
  case 5:
    System.out.println("Caso 4 o 5");
    break;
  default:
    System.out.println("Ninguno caso encaja");
    break;
  }
}
Podemos ver que hay una expresion de tipo int y cuatro casos (case) que se pueden ejecutar en función de su valor:
  1. case 1: Cuando expresion tenga valor 1
  2. case 2: case 3: Cuando expresion tenga valor 2 o 3
  3. case 4:
    case 5:
    Cuando expresion tenga valor 4 o 5
  4. default: Cuando no sea ninguno de los anteriores
Fijaros que para el case 2: case 3: NO hemos puesto break antes del siguiente case. No debemos olvidarnos de ponerlo si no queremos que se siga ejecutando todo el código del switch hasta que se encuentre un break o termine el bloque switch.

Al probar este método con el siguiente código:
testSwitch(0);
testSwitch(1);
testSwitch(3);
Tenemos la siguiente salida:

Ninguno caso encaja
Caso 1
Caso 2 o 3
Caso 4 o 5

Esto es el resultado de:
  • Ninguno caso encaja: 0 (cero) no está entre las opciones elegidas y se ejecuta default.
  • Caso 1: coincide y ejecuta case 1 hasta el break.
  • Caso 2 o 3 + Caso 4 o 5: coincide con case 3 pero al no haber puesto break se sigue ejecutando lo siguiente hasta que encuentra otro break.
Espero que haya quedado claro, si no, usa los comentarios.

15 de enero de 2019

Gradle + Eclipse + GitHub

En esta entrada voy a enumerar los pasos que hay que seguir para poder utilizar, en nuestro IDE Eclipse, un proyecto de Gradle con una dependencia directa de un repositorio en GitHub.

Con esto vamos a poder automatizar la construcción y pruebas de nuestra aplicación y tener las dependencias actualizadas (o establecerlas a una versión concreta) sin tener que cargar con ellas.

Eclipse, Gradle y GitHub forman una combinación perfecta para automatizar tareas de construcción y pruebas


Eclipse lo conocemos, GitHub también deberíamos, la nueva desconocida puede ser Gradle: es una herramienta de automatización de la construcción de código abierto. En esta entrada se verán términos como plugins o tasks, pero no se explican pues no es objeto de la entrada. Para más información se puede ver en su documentación.

Hay más herramientas de automatización de tareas como Maven y Ant de Apache. En Java personalmente creo que Gradle se está volviendo más fuerte, pero es una opinión. Maven y Ant llevan más tiempo y es probable que si cogéis proyectos más antiguos los usen.

En el ejemplo vamos a usar el repositorio en GitHub "jsonbeans" de EsotericSoftware para convertir objetos entre Java y JSON, pero serviría para cualquier otro. Nos vamos a traer la versión release de la rama master.

Para ello vamos a necesitar Eclipse con el plugin Gradle que viene por defecto en la versión que instalamos con nuestro tutorial y utilizar el servicio Jitpack que será el encargado de construir y suministrarnos bajo demanda artefactos (.jars) de repositorios de GitHub (o de otras plataformas parecidas) para cubrir esas dependencias.

Creamos un proyecto vacío siguiendo estos pasos:
  1. Abrimos Eclipse
  2. En Package Explorer: New/Project.../Gradle Project/Next
  3. Poner nombre al proyecto -> Next -> Next o escoger versión/Wrapper -> Finish
Con esto ya tenemos un proyecto "vacío" creado con una configuración por defecto. Vamos a personalizarlo para lo que queremos conseguir:
  1. Copiar la configuración del archivo build.gradle preparado y substituirlo (se añaden una serie de plugins y dependencias que se explican en comentario dentro del propio archivo)
  2. Modificar en build.gradle la clase con el main según corresponda: cambiar App a com.paquete.Library por ejemplo
Ahora tenemos que hacer que esos cambios sean visibles por Eclipse:
  1. Actualizar las tareas (tasks) del proyecto Gradle en pestaña "Gradle Tasks" (click en icono de refresco)
  2. Ahora nos aparecen nuevas tasks por los plugins que instalamos. Ejecutar la task (haciendo doble click sobre ella) ide/eclipse
  3. Refrescar el proyecto en el Package Explorer
  4. Comprobar que aparece jsonbeans-master.jar en Referenced Libraries
  5. Elegir para el proyecto la librería JDK que tengamos instalada (por ejemplo C:\Program Files (x86)\Java\jdk1.8.0_191). Hay multiples formas de hacerlo, mi preferida para compartir proyectos es configurarlo en el fichero gradle.properties (crearlo si no existe en la raiz del proyecto). Se hace añadiendo una línea (ejemplo):
org.gradle.java.home=C:/Program Files (x86)/Java/jdk1.8.0_191

NOTA 1: Es importante usar "/" en la ruta dentro de gradle.properties.
NOTA 2: Si al seguir estos pasos da un error, puede ser debido a la ubicación del archivo gradle.properties. Aunque en la documentación dice que debe ir en el raíz, se ha comprobado que algunas veces se soluciona un error en este procedimiento ubicándolo en la carpeta "src".

Comprobamos que todo ha ido bien y que el asistente de contenido busca en las nuevas dependencias (en Library mismo probar la clase Json). Podemos usar este código para el main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    public static void main(String[] args) {
        // Serializador JSON
        Json json = new Json(OutputType.json);

        // Objeto para serializar inicializado
        Object obj = new Object() {
            int[] numeros;
            String texto;

            Object init() {
                numeros = new int[] { 1, 2, 3 };
                texto = "Mi texto";

                return this;

            }
        }.init();

        // Salida por consola serializada en formato facil de leer
        System.out.println(json.prettyPrint(obj));
    }

NOTA: Si el código te parece extraño, mira la explicación. Se ha puesto como ejemplo para crear un objeto asignándole unos valores NO por defecto a una clase anónima con el mínimo código)

Finalmente ejecutamos para ver el resultado:
  1. Ejecutar task application/run y comprobar que funciona

De esta forma vamos a poder compartir muy facilmente el proyecto con sus dependencias sin tener que compartir los archivos compilados. Gradle se encargará por nosotros de controlar de manera centralizada los archivos de esas dependencias para todos los proyectos que gestionen (los cacheará en la carpeta GRADLE_HOME (se usará la instalación de Gradle local o la carpeta .gradle en la carpeta personal del usuario)

Compárteme

Entradas populares