1. Implementación de la encapsulación

1.1 Encapsulación

Los objetos conocen solamente su estructura, no la de los demás. El trato entre objetos se realiza a través de los métodos. Normalmente, los atributos de un objeto se deben consultar o editar a través de métodos.

1.2 Buenas prácticas.

1.2.1 Para clases

  • La mayoría de las clases que se crean son públicas.
  • Cada fichero .java tendrá solamente una clase pública, con el mismo nombre del fichero.

1.2.2 Para atributos

  • La mayoría de los atributos de una clase serán privados.
  • Solamente algunas constantes, o casos muy particulares, tendrán otra modificador de acceso.

1.2.3 Para métodos

  • Si una clase tiene atributos, seguramente tenga métodos públicos.
  • Los métodos privados son interesantes para cálculos auxiliares o parciales (solo se pueden invocar desde la propia clase).

1.3 Tipos de clases

Aunque Java tiene solamente una forma de crear clases, los patrones de diseño nos dicen que podemos encontrar diferentes tipos de clases según su cometido:

  • Modelo: representan objetos o hechos de la naturaleza: un coche, un asiento contable, los datos meteorológicos de un día. Suelen tener atributos, getters y setters, equals, hashCode, toString, …
  • Servicios: implementan la lógica de negocio. Suelen tener algunos atributos, pero sobre todo métodos públicos y privados.
  • Auxiliares: sirven para realizar operaciones auxiliares de cálculo o transformación de datos. Mayoritariamente, sus métodos son estáticos.
  • Main: son el punto de entrada de la aplicación. La mayoría de las ocasiones, solo tienen este método, y si tienen más, suelen ser estáticos.
  • Test: clases orientadas a realizar pruebas de nuestra aplicación. En Java, suelen ser test unitarios con JUnit.

2. Implementación de herencia con modificadores de acceso y composición

2.1 Herencia de clases

Una clase que extiende a otra hereda sus atributos y sus métodos (no constructores). Puede añadir atributos y métodos nuevos. Se trata de una asociación es-un, ya que la clase Hija es-un(a) (sub)tipo de la clase Base. Por ejemplo un Coche es-un Vehiculo, o un Leon es-un Animal.

Si usamos protected en la clase base, tendremos acceso directo a los atributos. En otro caso, tendremos que acceder vía getters/setters. ¡OJO! Los constructores no se heredan aunque sean públicos.

public class Base {
  private String nombre;
  protected String apellidos;
  //...
}

public class Hija extends Base {
  public void metodo() {
    //this.nombre = "Pepe"; //Imposible acceder. Nos da error
    this.setNombre("Pepe"); //Funciona perfectamente
    this.apellidos = "Perez";
  }
  //...
}

2.2 Herencia de interfaces

También podemos establecer relaciones jerárquicas entre interfaces. Nos regimos por las mismas reglas que en el caso de las clases.

2.3 Asociación de composición

Normalmente, cuando representamos la estructura de un sistema, está formado por muchas clases. En este caso, no solamente importan las clases en sí, sino las asociaciones. Una de ellas es la composición. En UML, se representan de una forma especial.

uml composicion

Dentro de la clase Todo tendremos una referencia a la clase Parte. También es posible que la multiplicidad nos indique que debemos tener una colección (Provincia y Municipio). Normalmente hay dependencia de existencia entre la parte y el todo.

3. Polimorfismo

3.1 Polimorfismo

Java nos permite crear instancias de objetos, pero que estos sean referenciados por alguna de sus clases ancestro o alguno de los interfaces que implementa:

Rectangulo r = new Cuadrado();

También permite la ocultación o sobreescritura de métodos por parte de las clases derivadas.

¿Qué sucede en caso de el uso de referencias de clase base, pero instanciación de objetos derivados? Java escoge, en tiempo de ejecución, el tipo de objeto. Si ese tipo ha ocultado un método de la superclase, llama a la concreción. En otro caso, llama al método de la clase base.

Cuadrado [lado=5.39897]
Area de un cuadrado
Perimetro de un cuadrado
Area de la figura: 10,797940 | Perímetro de la figura: 21,595881

Rectangulo [base=10.55045, altura=0.6183386]
Area de un rectángulo
Perímetro de un rectángulo
Area de la figura: 6,523750 | Perímetro de la figura: 22,337578

Esto también sucede cuando una clase implementa una interfaz que forma parte de una jerarquía de herencia:

ClaseQueImplementaInterfaz c1 = new ClaseQueImplementaInterfaz();
c1.saludar("Hola Mundo");

Hija c2 = new ClaseQueImplementaInterfaz();
c2.saludar("Hola Mundo, otra vez");

Base c3 = new ClaseQueImplementaInterfaz();
c3.saludar("Hola Mundo, por tercera vez");

4.Sobreescribir los métodos hashcode, equals y toString de la clase Object

4.1 Herencia de Object

Todo objeto, de forma directa o indirecta hereda de Object. Esta clase tiene una serie de métodos, entre los que destacan:

  • equals: nos permite indicar cuando dos objetos son iguales
  • hashCode: nos devuelve un número “único” asociado a una instancia de una clase
  • toString: nos devuelve una reperesentación del objeto como una cadena de caracteres.

4.2 Sobrescritura de equals

Con tipos primitivos, hemos usado el operador ==. ¿Qué sucede con los objetos? Primero tenemos que definir cuando dos instancias de un objeto son iguales o diferentes.

El método equals nos permite devolver un boolean indicando si un objeto es igual a otro. Nuestro IDE lo autogenera, junto con hashCode. La representación actual es muy verbosa y no aprovecha las capacidades de Java 7. La actualización está en desarrollo.

4.3 Sobrescritura de hashCode

Devuelve un número asociado a la clase. Sirve como posición de memoria en hexadecimal. Por definición, si dos objetos son iguales (equals), su hash code también debe serlo. Si sobrescribimos el método equals, también tenemos que sobrescribir hashCode para que se cumpla esa propiedad.

4.4 Sobrescritura de toString

Devuelve una representación en String del objeto. Por defecto, devuelve el tipo (la clase) y su hashCode. Lo podemos sobrescribir para que represente los valores. Dos objetos iguales deben tener la misma representación.

5. Uso del ámbito static para variables, métodos y clases

5.1 Atributos de objeto y de clase

Java nos permite crear cuantos objetos queramos de una misma clase. Estos objetos tienen una copia propia de los atributos. Sin embargo, en ocasiones, puede interesarnos tener un atributo común a todos los objetos. Estos son los atributos static.

Están asociados a la clase, y compartidos para todas las instancias. Pueden ser manipulados por cualquier objeto, o incluso sin crear una instancia de esa clase.

5.2 Métodos estáticos

Se rigen por los mismos principios de los atributos estáticos. Para invocar:

Clase.metodoEstatico(...);

Podemos acceder a una variable estática desde un contexto estático o no estático. También podemos crear métodos estáticos como métodos de operaciones auxiliares (recordemos todos los métodos de la clase java.util.Arrays).

5.3 Constantes

Se pueden definir como static.

static final PI = 3.141592653589793;

5.4 Clases estáticas

Este tipo de clases solamente tienen sentido cuando trabajemos con clases internas (lo haremos en capítulos posteriores). Nos permiten, entre otras cosas, agrupar código, definiendo clases que tendrán solo sentido si están envueltas por otra.

Para crear una instancia de la clase interna, si esta es estática, no necesitamos una instancia de la clase externa:

public class EjemploClaseStatic {
  /**
  * @param args
  */
  public static void main(String[] args) {
    Persona p = new Persona("Pepe");
    System.out.println(p);
  }

  public static class Persona {
    //...
  }

  public class OtroEjemplo {
    /**
    * @param args
    */
    public static void main(String[] args) {
      EjemploClaseStatic.Persona p = new EjemploClaseStatic.Persona("Pepe");
      System.out.println(p);
    }
  }
}

6. Clases singleton y clases de inmutables

6.1 Singleton

Singleton es uno de los patrones de diseño propuesto por Gang of Four (GOF), y sirve para poder tener una clase de la cual solamente querremos tener una instancia (manejadores, servicios, …).

Para implementarla, podemos seguir los siguientes pasos:

  1. Definir un único constructor, como privado, para evitar instanciaciones innecesarias.
  2. Obtener siempre la instancia a través de un método estático.
public class MiServicioSingleton {
  //Una instancia del objeto que va a existir
  private static MiServicioSingleton instance = null;

  //Evitamos así la instanciación directa
  private MiServicioSingleton() { }

  public static MiServicioSingleton getInstance() {
    if (instance == null)
      instance = new MiServicioSingleton();
    return instance;
  }
}

6.2 Objetos inmutables

Son objetos cuyo estado no puede ser modificado una vez se haya inicializado. No son constantes ya que estas se definen en tiempo de compilación, y los inmutables en tiempo de ejecución. Un ejemplo de clase inmutable que ya hemos utilizado es String.

Algunas recomendaciones para crear objetos inmutables:

  1. Definir todas las propiedades como final private
  2. No añadir métodos setter
  3. Evitar que existiendan la clase, añadiendole el modificador final a la definición.

7. Clases y métodos abstractos

7.1 Abstract

Es una palabra reservada, que puede usarse a nivel de método o de clase. Sirve para indicar la obligación de implementar un método o de extender una clase completa.

7.2 A nivel de clase

Son clases que no se pueden instanciar. Puede tener métodos con implementación y atributos, y también métodos abstractos.

public abstract class ObjectoGrafico implements Transformable {

  protected int x, y;

  public void moverA(int nuevaX, int nuevaY) {
    this.x = nuevaX;
    this.y = nuevaY;
  }

  abstract public void dibujar();

  abstract public void cambiarTamanio(int factorAumento);

}

 

7.3 A nivel de método

Los métodos definidos como abstract deben estar en una clase abstracta. Definen la firma del método, pero sin implementación. Sus subclases se comprometen a implementarlo. Si no lo hacen, también deben ser abstractas. Pueden convivir con métodos normales.

7.4 Clases abstractas que implementan interfaces

Una clase que implementa una interfaz tiene obligación de implementar todos sus métodos. Sin embargo, una clase abstract puede dejar métodos sin implementación, obligando a quienes la extiendan a hacerlo.

public interface Transformable { 
  public void rotar(); 
  public void voltearHorizontal(); 
  public void voltearVertical(); 
} 

public abstract class ObjectoGrafico implements Transformable { 
  protected int x, y; 
  public void moverA(int nuevaX, int nuevaY) { 
    this.x = nuevaX; 
    this.y = nuevaY; 
  } 

  abstract public void dibujar(); 
  abstract public void cambiarTamanio(int factorAumento); 
}

8. Código que usa final

8.1 Modificador final

Se puede utilizar en diferentes contextos:

  • Clases final
  • Métodos final
  • Variables final

En todos los casos, indica que de una u otra forma, el ámbito sobre el que aplica no podrá ser modificado.

8.2 Clases final

Son clases que no se pueden extender. En una jerarquía de herencia, son el último nodo. Se pueden instanciar y tratar con normalidad.

8.3 Métodos final

Se definen en clases susceptibles de ser extendidas. Nos permiten indicar que un método no se va a poder sobrescribir. En la clase extendida habrá que usar, obligatoriamente, la implementación de la clase base.

8.4 Variables final

Basicamente indican que aquella variable a la que afectan no se puede modificar. Podemos diferenciar entre:

  • Tipos primitivos: serán valores inmodificables, constantes. Suele usarse junto con static.
  • Objetos: si declaramos una referencia como final, estamos diciendo que esa referencia no podrá asignarse a otro objeto. Sin embargo sí que podemos modificar el estado del objeto con sus propios métodos. Lo mismo sucedería en el caso de arrays.

9. Clases internas, locales y anónimas

9.1 Clases dentro de otras clases

Java permite definir clases dentro de otras clases. A estas clases se le llaman anidadas. Pueden ser de dos tipos, estáticas o no estáticas. No se trata de composición de clases, sino anidamiento. En algunos casos, pueden acceder a los atributos de la clase que le envuelve.

Las razones para su uso son varias:

  • Agrupamiento lógico de clases que se utilizan en un solo lugar. Por tanto hay mayor cohesión.
  • Aumento de la encapsulación.
  • Código más legible y fácil de mantener.

9.2 Clases internas

Se llaman así a las clases anidadas no estáticas. Solo pueden existir en el marco de una instancia de la clase externa. Pueden acceder a sus miembros (de la clase externa).

Si definimos una variable miembro en la clase interna, con el mismo nombre otra de la clase externa, la interna oculta a la externa. A esto se le llama shadowing

9.3 Clases locales

Son clases que se definen dentro de un bloque, normalmente el cuerpo de un método. Sirven para afinar aun más la cohesión del código.

9.4 Clases anónimas

Permiten definir e instanciar una clase a la vez. Son como clases locales sin nombre. Sirven para ser usadas una vez.

Las podemos definir a partir de otra clase o de una interfaz. Podemos crearlas en el cuerpo de un método, de una clase, o como argumento de un método.

10. Uso de enumeraciones

10.1 Tipos enumerados

Son un tipo de dato especial. Indica que una variable tendrá como valor uno de entre un conjunto cerrado, como por ejemplo Direccion (Norte, Sur, Este, Oeste).

public enum Direccion {
  NORTE, SUR, ESTE, OESTE
}

En Java, los tipos enumerados son más potentes que en otros lenguajes. Para Java son un tipo de clase, que pueden incluir métodos y otros atributos. De hecho, el compilador añade métodos especiales (values), que incluso nos permite recorrer todas las instancias. Podemos pensar en que tenemos un conjunto cerrado de instancias de una clase.

11. Creación de una clase genérica

11.1 Clases genéricas

Java permite desde sus orígenes usar clases genéricas, utilizando referencias de tipo Object. Sin embargo, estas pueden producir problemas en tiempo de ejecución.

public class Box {
  private Object object;

  public void set(Object object) {
    this.object = object;
  }

  public Object get() {
    return object;
  }
}

 

Desde Java SE 5, podemos crear clases cuyo tipo se indica en tiempo de compilación

public class Box<T> {
  private T object;

  public void set(T object) {
    this.object = object;
  }

  public T get() {
    return object;
  }
}

Podemos utilizar más de un tipo diferente a la vez:

public class Par<T, S> {
  private T obj1;
  private S obj2;

  //Resto de la clase
}

11.1.1 Nomenclatura con los tipos

  • E (element, elemento)
  • K (key, clave)
  • N (number, número)
  • T (type, tipo)
  • V (value, valor)
  • S, U, V, … (2º, 3º, 4º, … tipo)

11.2 Instanciación y operador diamond

Hasta Java SE 6, para instanciar un objeto genérico, tenemos que indicar los tipos dos veces.

Par<String, String> pareja2 = new Par<String, String>("Hola", "Mundo");

Desde Java SE 7, tenemos el operador <> diamond:

Par<String, String> pareja2 = new Par<>("Hola", "Mundo");

11.3 Clases genéricas con tipos cerrados

Podemos acotar el tipo parametrizado, para que sea uno en particular o sus derivados:

public class NumericBox<T extends Number> {

  private T object;

  //resto de la clase
}

Se puede indicar más de un tipo. Uno de ellos (y solo uno) se corresponderá con una clase; el resto deben ser interfaces. La clase a extender debe ser la primera de la lista:

public class A {
  //resto de la clase
}

public interface B {
  //resto de la interfaz
}

public class StrangeBox <T extends A & B> {

  //resto de la clase
}

11.4 Genéricos con tipos comodín

Los tipos comodín nos permiten relajar el tipo concreto de una clase genérica a un subtipo. Son muy útiles en el caso de trabajar con colecciones (las trataremos en los próximos capitulos).

public static double sumOfList(List<? extends Number> list) {
  double s = 0.0;
  for (Number n : list)
    s += n.doubleValue();
  return s;
}

12. Creación y uso de list, set y map

12.1 API de colecciones

Desde Java SE 2 se ofrece el tratamiento de colecciones. Actualmente tiene

  • Interfaces: tipos de datos
  • Implementaciones: concreciones de los diferentes interfaces.
  • Algoritmos: para realizar operaciones como ordenación, búsqueda, …

Actualmente, todas las colecciones están definidas como genéricas.

12.2 Tipos de colecciones

Java propone diferentes tipos de colecciones, a través de varias interfaces. Nosotros trabajaremos en esta lección con 3:

  • List: Se trata de una estructura lineal, con posibilidad de orden y de repetidos.
  • Set: es una colección que no soporta duplicados, y con posibilidad de orden.
  • Map: es una estructura de tipo clave, valor, con posibilidad de orden de los elementos (por la clave)

12.3 Intefaz List

Los elementos tienen siempre una posición, y permite duplicados. También permite búsqueda e iteraciones. Las implementaciones más conocidas son ArrayList y LinkedList. Si no sabemos cual escoger, utilizaremos siempre ArrayList.

Para construir una instacia, desde Java SE 7 podemos usar el operador diamond:

List<String> cars = new ArrayList<>();

12.4 Interfaz Set

Se trata de una colección que no puede contener repetidos. Java propone tres implementaciones: HashSetTreeSet y LinkedHastSet:

  • HashSet es la más eficiente, pero no nos asegura nada sobre el orden.
  • TreeSet utiliza un árbol Red-Black, y ordena según el valor.
  • LinkedHashSet es un HashSet ordenado por orden de inserción.

12.5 Interfaz Map

No es un subtipo de Collection (List y Set sí que lo son). Cada elemento tiene estructura clave, valor. La clave sirve para acceder directamente al valor. Las implementaciones son HashMapTreeMap y LinkedHashMap. Las consideraciones son análogas a Set.

13. Interfaces Comparable y Comparator

13.1 Introducción

Muchas operaciones entre objetos nos obligan a compararlos: buscar, ordenar, … Si bien los tipos primitivos y algunas clases ya implementan su orden (natural, lexicográfico), para nuestras clases (modelo) tenemos que especificar el orden con el que las vamos a tratar.

13.2 Comparable

Comparable es un interfaz propuesto por Java, y su definición es sencilla:

public interface Compararable<T> {
     public int compareTo(T o);
}

Recibe un objeto del mismo tipo que la clase que lo implementa. El valor de retorno del método compareTo será:

  • 0 si ambos objetos son iguales,
  • un valor negativo si el objeto es menor,
  • y uno positivo si es mayor.

Nos sirve para indicar el orden principal de una clase.

13.3 Comparator

Comparator también es un interfaz propuesto por Java, y su definición también es sencilla:

public interface Comparator<T> {
     public int compare(T o1, T o2);
}

Recibe dos argumentos, y su valor de retorno es análogo al de comparable.

Comparator nos servirá para indicar un orden diferente al orden natural definido con Comparable (no es necesario haber definido un orden con Comparable para poder utilizar Comparator, aunque sí es recomendable).

14. Interfaces funcionales

14.1 Interfaces

Una interfaz es un contrato que compromete a la clase que lo implementa a dar cuerpo a una serie de métodos abstractos. Además, se pueden utilizar como referencias a la hora de crear objetos (que implementen esa interfaz, claro está):

List<String> lista = new ArrayList<>();

Desde Java SE 8, las interfaces pueden incluir la implementación de algunos métodos, en particular, los métodos anotados con default y static.

public interface Interfaz {

  public void metodo();

  default public void metodoPorDefecto() {
    System.out.println("Este es uno de los nuevos métodos por defecto");
  }

  public static void metodoEstatico() {
    System.out.println("Método estático en un interfaz");
  }
}

14.2 Interfaces funcionales

Una interfaz funcional será una interfaz que solamente tenga la definición de un método abstracto. Estrictamente hablando, puede tener varios métodos abstractos, siempre que todos menos uno sobrescriban a un método público de la clase Object. Además, pueden tener uno o varios métodos por defecto o estáticos.

Normalmente, son interfaces que implementamos mediante una clase anónima. Muchos de los interfaces que conocemos, como por ejemplo Comparator, son interfaces funcionales:

Collections.sort(lista, new Comparator<String>() {

  //Ordenamos la cadena por su longitud
  @Override
  public int compare(String str1, String str2) {
    return str1.length()-str2.length();
  }
});

Java SE 8 incorpora también la anotación @FunctionalInterface que permite al compilador comprobar si una interfaz cumple con las características de ser funcional o no (Eclipse nos proporciona dicha funcionalidad en directo, a la par de escribir el código).

Las interfaces funcionales y las expresiones lambda están áltamente ligadas, de forma que allá donde se espere una instancia de una clase que implemente una interfaz funcional, podremos utilizar una expresión lambda.

Collections.sort(lista, (str1, str2)-> str1.length()-str2.length());

15. Predicate, consumer, function y supplier

15.1 Predicate<T>

El método abstracto es:

boolean test(T t);

Comprueba si se cumple o no una condición. Se utiliza mucho junto a expresiones lambada a la hora de filtrar:

//...
    .filter((p) -> p.getEdad() >= 35l)

Se pueden componer predicados más complejos con sus métodos andor y negate.

15.2 Consumer<T>

El método abstracto es:

void accept(T t);

Sirve para consumir objetos. Uno de los ejemplos más claros es imprimir.

//...
    .forEach(System.out::println)

Adicionalmente, tiene el método andThen, que permite componer consumidores, para encadenar una secuencia de operaciones.

15.3 Function<T, R>

El método abstracto es:

R apply(T t);

Sirve para aplicar una transformación a un objeto. El ejemplo más claro es el mapeo de objetos en otros.

//...
    .map((p) -> p.getNombre())

Adicionalmente, tiene otros métodos:

  • andThen, que permite componer funciones.
  • compose, que compone dos funciones, a la inversa de la anterior.
  • identity, una función que siempre devuelve el argumento que recibe

15.4 Supplier<T>

El método abstracto es:

T get();

Sirve para devolver un valor.

Tiene algunos interfaces especializados para tipos básicos:

  • IntSupplier
  • LongSupplier
  • DoubleSupplier
  • BooleanSupplier

16. Introducción al API Stream

16.1 Introducción

El API Stream es una de las grandes novedades de Java SE 8, junto con las expresiones lambda. Permite realizar operaciones de filtro/mapeo/reducción sobre colecciones de datos, de forma secuencial o paralela.

Un Stream es una secuencia de elementos que soporta operaciones para procesarlos

  • Usando expresiones lambda
  • Permitiendo el encadenamiento de operaciones (para producir así un código que se lee mucho mejor y es más conciso)
  • De forma secuencial o paralela

En Java, los streams vienen definidos por el interfaz java.util.stream.Stream<T>.

16.2 Características de un Stream

  • Las operaciones intermedias retornan un Stream (permitiendo así el encadenamiento de llamadas a métodos).
  • Las operaciones intermedias se encolan, y son invocadas al invocar una operación terminal.
  • Solo se puede recorrer una vez; si lo intentamos recorrer una segunda vez, provocará una excepción.
  • Utiliza iteración interna en lugar de iteración externa; así nos centramos en qué hacer con los datos, no en como recorrerlos.

16.3 Algunos subtipos de streams

En el caso de que vayamos a utilizar un stream de tipos básicos (intlong y double), Java nos proporciona las interfaces IntStreamLongStream y DoubleStream.

16.4 Formas de obtener un stream

  • Stream.of(...): retorna un stream secuencial y ordenado de los parámetros que se le pasan.
  • Arrays.streams(T[] t): retorna un stream secuencial a partir del array proporcionado. Si el array es de tipo básico, se retorna un subtipo de Stream.
  • Stream.empty(): retorna un stream vacío.
  • Stream.iterate(T, UnaryOperator<T>): devuelve un stream infinito, ordenado y secuencial. Lo hace a partir de un valor y de aplicar una función a ese valor. Se puede limitar el tamaño con limit(long).
  • Collection.stream() y Collection.parallelStream(): devuelve un stream (secuencial o paralelo) a partir de una colección.
  • Collection.generate: retorna un stream infinito, secuencial y no ordenado a partir de una instancia de Supplier (o su correspondiente expresión lambda).

16.5 Operaciones intermedias

Son operaciones que devuelven un Stream, y por tanto, permiten encadenar llamadas a métodos. Sirven, entre otras funcionalidades, para filtrar y transformar los datos.

16.5.1 Operaciones de filtrado

  • filter(Predicate<T>): nos permite filtrar usando una condición.
  • limit(n): nos permite obtener los n primeros elementos.
  • skip(m): nos permite obviar los m primeros elementos.

16.5.2 Operaciones de mapeo

  • map(Function<T,R>): nos permite transformar los valores de un stream a través de una expresión lambda o una instancia de Function.
  • mapToInt(...)mapToDouble(...) y mapToLong(...) nos permite transformar a tipos básicos, obteniendo IntStreamDoubleStream o LongStream, respectivamente.

16.6 Operaciones terminales

Provocan que se ejecuten todas las operaciones intermedias. Las hay de varios tipos:

  • Para consumir los elementos (por ejemplo, forEach)
  • Para obtener datos de un stram (agregación)
  • Para recolectar los elementos y transformarlos en otro objeto, como una colección.

17. Métodos de búsqueda de datos

17.1 Métodos de búsqueda

Son un tipo de operaciones terminales sobre un stream, que nos permiten:

  • Identificar si hay elementos que cumplen una determinada condición
  • Obtener (si el stream contiene alguno) determinados elementos en particular.

Algunos de los métodos de búsqueda son:

  • allMatch(Predicate<T>): verifica si todos los elementos de un stream satisfacen un predicado.
  • anyMatch(Predicate<T>): verifica si algún elemento de un stream satisface un predicado.
  • noneMatch(Predicate<T>): opuesto de allMatch(…)
  • findAny(): devuelve en un Optional<T> un elemento (cualquiera) del stream. Recomendado en streams paralelos.
  • findFirst() devuelve en un Optional<T> el primer elemento del stream. NO RECOMENDADO en streams paralelos.

18. Métodos de datos, cálculo y ordenación

18.1 Métodos de datos y cálculo

Los streams nos ofrecen varios tipos de métodos terminales para realizar operaciones y cálculos con los datos. Durante el curso trabajaremos con tres tipos:

  • Reducción y resumen (en esta lección)
  • Agrupamiento
  • Particionamiento

18.2 Métodos de reducción

Son métodos que reducen el stream hasta dejarlo en un solo valor.

  • reduce(BinaryOperator<T>):Optional<T> realiza la reducción del Stream usando una función asociativa. Devuevle un Optional
  • reduce(T, BinaryOperator<T>):T realiza la reducción usando un valor inicial y una función asociativa. Se devuelve un valor como resultado.

18.3 Métodos de resumen

Son métodos que resumen todos los elementos de un stream en uno solo:

  • count: devuelve el número de elementos del stream.
  • min(...), max(...): devuelven el máximo o mínimo (se puede utilizar un Comparator para modificar el orden natural).

18.4 Métodos de ordenación

Son operaciones intermedias, que devuelven un stream con sus elementos ordenados.

  • sorted() el stream se ordena según el orden natural.
  • sorted(Comparator<T>) el stream se ordena según el orden indicado por la instancia de Comparator.

19. Uso de Map y flapMap

19.1 Uso de map

map es una de las operaciones intermedias más usadas, ya que permite la transformación de un objeto en otro, a través de un Function<T, R>. Se invoca sobre un Stream<T> y retorna un Stream<R>. Además, es muy habitual realizar transformaciones sucesivas.

lista
   .stream()
   .map(Persona::getNombre)
   .map(String::toUpperCase)
   .forEach(System.out::println);

19.2 Uso de flatMap

Los streams sobre colecciones de un nivel permiten transformaciones a través de map pero, ¿qué sucede si tenemos una colección de dos niveles (o una dentro de objetos de otro tipo)?:

public class Persona {

    private String nombre;
    private List<Viaje> viajes = new ArrayList<>();

  //resto de atributos y métodos
}

Para poder trabajar con la colección interna, necesitamos un método que nos unifique un Stream<Stream<T>> en un solo Stream<T>. Ese es el cometido de flatMap.

lista
   .stream()
   .map((Persona p) -> p.getViajes())
   .flatMap(viajes -> viajes.stream())
   .map((Viaje v) -> v.getPais())
   .forEach(System.out::println);

También tenemos disponibles las versiones primitivas flatMapToIntflatMapToLong y flatMapToDouble:

Arrays
    .stream(numeros)
    .flatMapToInt(x -> Arrays.stream(x))
    .map(IntUnaryOperator.identity())
    .distinct()
    .forEach(System.out::println);

19.3 Collectors

Los collectors nos van a permitir, en una operación terminal, construir una collección mutable, el resultado de las operaciones sobre un stream.

19.3.1 Colectores “básicos”

Nos permiten operaciones que recolectan todos los valores en uno solo. Se solapan con alguans operacinoes finales ya estudiadas, pero están presentes porque se pueden combinar con otros colectores más potentes.

  • counting: cuenta el número de elementos.
  • minBy(…)maxBy(…): obtiene el mínimo o máximo según un comparador.
  • summingIntsummingLongsummingDouble: la suma de los elementos (según el tipo).
  • averagingIntaveragingLongaveragingDouble: la media (según el tipo).
  • summarizingIntsummarizingLongsummarizingDouble: los valores anteriores, agrupados en un objeto (según el tipo).
  • joinning: unión de los elementos en una cadena.

19.3.2 Colectores “grouping by”

Hacen una función similar a la cláusula GROUP BY de SQL, permitiendo agrupar los elementos de un stream por uno o varios valores. Retornan un Map.

Map<String, List<Empleado>> porDepartamento =
   empleados
     .stream()
     .collect(groupingBy(Empleado::getDepartamento));

Se pueden usar en conjunción cno los colectores básicos, o con otro colector grouping by:

Map<String, Long> porDepartamentoCantidad =
   empleados
      .stream()
      .collect(groupingBy(Empleado::getDepartamento, counting()));

Map<String, Map<Double, List<Empleado>>> porDepartamentoYSalario =
   empleados
     .stream()
     .collect(groupingBy(Empleado::getDepartamento, groupingBy(Empleado::getSalario)));

También tenemos los colectores partitioning, que nos agrupan los resultados dos conjuntos, según si cumplen una condición:

Map<Boolean, List<Empleado>> salarioMayorOIgualque32000 =
   empleados
     .stream()
     .collect(partitioningBy(e -> e.getSalario() >= 32000));

19.3.3 Colectores “Collection”

Producen como resultado una colección: List, Set y Map.

Set<Empleado> setEmpleados = empleados.stream().collect(Collectors.toSet());
List<Empleado> listEmpleados = empleados.stream().collect(Collectors.toList());
Map<String, Double> mapEmpleados = empleados.stream().distinct()
                .collect(Collectors.toMap(Empleado::getNombre, Empleado::getSalario));

20. Uso de streams y filtros

filter es una operación intermedia, que nos permite eliminar del stream aquellos elementos que no cumplen con una determinada condición, marcada por un Predicate<T>.

personas
            .stream()
            .filter(p -> p.getEdad() >= 18 && p.getEdad() <= 65)
            .forEach(persona -> System.out.printf("%s (%d años)%n", persona.getNombre(), persona.getEdad()));

Es muy combinable con algunos métodos como findAny o findFirst:

Persona p1 = personas
                        .stream()
                        .filter(p -> p.getNombre().equalsIgnoreCase("Andrés"))
                        .findAny()
                        .orElse(new Persona());

Y se puede usar también en streams sobre colecciones tipo Map.

Map<Integer, Persona> personas = new HashMap<>();
//Inicialización
personas.entrySet()
            .stream()
            .filter(map -> map.getKey() >= 5)
            .collect(Collectors.toMap(p -> p.getKey(), p -> p.getValue()))
            .forEach((key, value) -> System.out.printf("%d: %s%n", key, value.getNombre()));

21. Referencias a métodos con stream

21.1 Referencias a métodos

Las referencias a métodos son una forma de hacer nuestro código aun más conciso:


public class Persona {
//…
 public static int
   compararPorEdad(Persona a, Persona b) {
      return a.fechaNacimiento
             .compareTo(b.fechaNacimiento);
 }
}

//...

List<Persona> personas = //...

// De menos a más "conciso"
personas.sort((Persona p1, Persona p2) -> {
   return p1.getFechaNacimiento()
      .compareTo(p2.getFechaNacimiento());
});

personas.sort((p1, p2) -> p1.getFechaNacimiento()
            .compareTo(p2.getFechaNacimiento()));

personas.sort(Persona::compararPorEdad);

21.2 Tipos de referencias a métodos

  • Clase::metodoEstatico: referencia a un método estático.
  • objeto::metodoInstancia: referencia a un método de instancia de un objeto concreto.
  • Tipo::nombreMetodo: referencia a un método de instancia de un objeto arbitrario de un tipo en particular.
  • Clase::new: referencia a un constructor.

 

Java I/O y NIO.2

23.1 Flujos

Son un canal de comunicación de las operaciones de entrada salida. Este esquema nos da independencia para poder trabajar igual tanto si estamos escribiendo en un fichero, como en consola, o si estamos leyendo de teclado, o de una conexión de red.

23.2 Tipos de Flujos

Dependiendo de su destino, tenemos:

  • Flujos de entrada: sirven para introducir datos en la aplicación.
  • Flujos de salida: sirven para sacar datos de la aplicación.

Dependiendo del contenido del flujo:

  • Flujos de bytes: manejan datos en crudo.
  • Flujos de caracteres: manejan caracteres o cadenas.

23.3 Flujos de salida

23.3.1 Patrón básico de los flujos de salida

Abrir el flujo
Mientras hay datos que escribir
  Escribir datos en el flujo
Cerrar el flujo

23.3.2 Flujos de salida de bytes

Algunas de las clases que podemos usar son:

  • OutputStream: clase abstracta, padre de la mayoría de los flujos de bytes.
  • FileOutputStream: flujo que permite escribir en un fichero, byte a byte.
  • BufferedOutputStream: flujo que permite escribir grupos (buffers) de bytes.
  • ByteArrayOutputStream: flujo que permite escribir en memoria, obteniendo lo escrito en un array de bytes.

23.3.3 Flujos hacia otros flujos

Solo FileOutputStream tiene un constructor que acepta una ruta (entre otras opciones). El resto reciben en sus constructores un tipo de OutputStream. ¿Por qué? Porque podemos construir flujos que escriben en flujos (encadenados).

BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("fichero.dat"));

23.3.4 Flujos de salida de caracteres

  • Writer: clase abstracta, padre de la mayoría de los flujos de caracteres.
  • FileWriter: flujo que permite escribir en un fichero, caracter a caracter.
  • BufferedWriter: flujo que permite escribir líneas de texto.
  • StringWriter: flujo que permite escribir en memoria, obteniendo lo escrito en un String
  • OutputStreamWriter: flujo que permite transformar un OutputStream en un Writer.
  • PrintWriter: flujo que permite escribir tipos básicos Java.

24.1 Patrón básico de los flujos de entrada

Abrir el flujo
Mientras hay datos que leer
  Leer datos del flujo
  Procesar los datos
Cerrar el flujo

24.2 Flujos de entrada de bytes

Algunas de las clases que podemos usar son:

  • InputStream: clase abstracta, padre de la mayoría de los flujos de bytes.
  • FileInputStream: flujo que permite leer de un fichero, byte a byte.
  • BufferedInputStream: flujo que permite leer grupos (buffers) de bytes.
  • ByteArrayInputStream: flujo que permite leer de memoria (de un array de bytes).

24.3 Flujos de entrada de caracteres

Algunas de las clases que podemos usar son:

  • Reader: clase abstracta, padre de la mayoría de los flujos de caracteres.
  • FileReader: flujo que permite leer de un fichero, caracter a caracter.
  • BufferedReader: flujo que permite leer líneas de texto.
  • StringReader: flujo que permite leer desde la memoria.
  • InputStreamReader: flujo que permite transformar un InputStream en un Reader.

 

34.1 Introducción

Java nos provee de soporte para conectar con bases de datos relacionales. Dicho soporte lo hace a través de la tecnología JDBC (Java DataBase Connectivity). Java SE 8 nos ofrece la versión 4.2 (JSR 211).

JDBC es un conjunto de interfaces, es decir, un contrato de nuestra forma de trabajar. Posteriomente necesitaremos un driver, es decir, un conjunto de clases que implementen esas interfaces, para darnos conexión con un sistema gestor de base de datos concreto.

34.2 Drivers

JDBC nos ofrece soporte para muchos SGBD comerciales a través de sus drivers. La lista completa la podemos encontrar en http://www.oracle.com/technetwork/java/index-136695.html. En estos capítulos nosotros trabajaremos con Mysql, cuyo driver lo podemos descargar en https://dev.mysql.com/downloads/connector/j/5.1.html .

34.3 Interfaces principales

  • Connection: es el que permite mantener la conexión con la base de datos.
  • StatementPreparedStatement: nos permiten ejecutar consultas.
  • ResultSet: juego de resultados de una consulta ejecutada.

34.4 Conexión con la base de datos

JDBC nos ofrece dos posibilidades para conectar con una base de datos.

  • Usar la clase DriverManager, que nos permite conectar a través de una url jdbc, y soporta varias operaciones.
  • Usar el interfaz DataSource, que es más avanzado, y que permite ser transparente a nuestra aplicación. Es más complejo que DriverManager.

NUESTRA ELECCIÓN

A lo largo de estos capitulos utilizaremos DriverManager. Aunque no es obligatorio, el uso de DataSource está orientado a proyectos Java EE. DriverManager será suficientemente potente para pequeños proyectos. Para otros más grandes, sería recomendable usar un sistema de persistencia como JPA.

34.4.1 URLs JDBC

Se trata de una cadena de texto con los datos de conexión a la base de datos. Dependerá del driver (base de datos) que vayamos a utilizar.

Por ejemplo, para conectar con Mysql, una URL tipo sería:

jdbc:mysql://hostname/database

34.4.2 Pasos para conectar, lanzar consultas y desconectar

Aunque las consultas las trabajaremos en profundidad en el siguiente capítulo, estos son los pasos tipo para conectar, lanzar consultas, procesar los resultados y desconectar.

  1. Cargar el driver JDBC (< 4.0)
  2. Establecer datos de conexión
  3. Conectar obteniendo un objeto Connection.
  4. Crear un objeto Statement y ejecutar consultas SQL
  5. Los resultados se almacenan en un objeto ResultSet, donde se pueden consultar.
  6. Cerrar los objetos (ResultSetStatement y Connection).

35.1 Introducción

JDBC nos provee de varios objetos para lanzar consultas, y uno para poder procesar los resultados.

  • Statement: nos permite lanzar consultas y recoger el resultado. Es la forma más básica de realizarlo.
  • PreparedStatement: permite lanzar consultas a las que podemos asignarle los valores de los parámetros mediante métodos convenientes.
  • CallableStatement: nos permite lanzar la ejecución de procedimientos almacenados y recoger sus resultados.
  • ResultSet: es la clase que nos permite recoger el resultado de la ejecución de una consulta realizada con alguno de los interfaces anteriores.

35.2 Statement

Nos provee de método para ejecutar consultas en una base de datos relacional. Recibe la consulta como un String, y devuelve (generalmente) un objeto de tipo ResultSet.

Tiene, entre otros, 3 métodos para ejecutar consultas:

  • execute: permite obtener más de un ResultSet
  • executeQuery: permite obtener un ResultSet
  • executeUpdate: Devuelve un entero que representa el número de filas afectadas. Se usa con consultas INSERT, UPDATE o DELETE

35.3 ResultSet

Se trata del objeto que nos permite recoger los resultados de una consulta SELECT. Tiene la estructura de un cursor, es decir, un juego de resultados y un puntero que nos permite navegar resultado resultado. Además, nos provee de métodos getXXX, por cada tipo de dato que podemos rescatar. Hay dos implementaciones por cada tipo de dato, una que recibe el índica de la columna, y otra que recibe el nombre.

Para recorrerlo de forma completa, lo normal suele ser utilizar el método next() con la siguiente estructura:

while (rs.next()) {
   String coffeeName = rs.getString(1);
   int supplierID = rs.getInt(2);
   float price = rs.getFloat(3);
   int sales = rs.getInt(4);
   int total = rs.getInt(5);
   //…
}

Tiene otros métodos para navegar. Además, en algunos casos (depende de la implementación del driver), tiene la posibilidad de trabajar en modo escritura, si bien no lo recomendamos desde aquí, ya que es preferible trabajar con otros esquemas, como el uso de PreparedStatement y el patrón de diseño DAO.

35.4 PreparedStatement

Se trata de una extensión de Statement que nos permite asignar los tipos de dato de los parámetros y sus valores, de forma conveniente.

Con un Statement, si quisiéramos buscar a todos los empleados con un sueldo mayor a un valor, tendríamos que hacerlo así:

float valor = 1235.34f;
String sql = "SELECT * FROM empleados WHERE SUELDO >= " + valor;

Frente a este esquema, que puede suponer problemas, PreparedStatement nos permite indicar, allí donde vamos a insertar un valor, indicar un interrogante, y darle valor después:

float valor = 1235.34f;
String sql = "SELECT * FROM empleados WHERE SUELDO >= ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setFloat(1, valor);

 

36.1 Introducción

Un RowSet no es más que un objeto que nos permite manejar información tabular de forma mucho más rápida y eficiente que un ResultSet. Existen 5 tipos de referencias y 2 tipos:

  • CONECTADOS
    • JdbcRowSet
  • DESCONECTADOS
    • CachedRowSet
    • JoinRowSet
    • FilteredRowSet
    • WebRowSet.

36.2 ¿En qué se diferencian de un ResultSet?

Básicamente en que pueden funcionar como JavaBeans, y por ende, beneficiarse del mecanismo de notificación que tienen estos (sistema de eventos y manejadores de los mismos), así como del uso de properties.

Por otro lado, con los ResultSet podíamos tener la posibilidad de hacer scroll (libre, no solo hacia adelante) o utilizarlos en modo escritura, pero solo en algunos tipos de drivers. RowSet nos asegura el poder realizarlo con cualquier sistema gestor de bases de datos.

36.3 Creación de un RowSet

Hasta Java SE 6, teníamos diferentes mecanismos para realizarlos. Java SE 7 nos incorpora uno más limpio, utilizando una factoría.

RowSetFactory myRowSetFactory = null;
JdbcRowSet rowSet = null;

myRowSetFactory = RowSetProvider.newFactory();
rowSet = myRowSetFactory.createJdbcRowSet();

La factoría nos permite crear cualquiera de las 5 interfaces que ofrece JDBC.

36.4 JdbcRowSet

Este tipo de RowSet es el único que mantiene siempre la conexión de la base de datos abierta. Esto tiene como ventaja la velocidad e inmediatez de los cambios. Como desventaja, dicho esquema de trabajo suele ser muy costoso (hablando en términos de recursos, sobre todo en el servidor de bases de datos).

Nos permite realizar operaciones como recorrer los resultados, actualizarlos, insertar nuevos, borrar, …

//...

myRowSetFactory = RowSetProvider.newFactory();
rowSet = myRowSetFactory.createJdbcRowSet();

rowSet.setUrl(DBConnection.JDBC_URL);
rowSet.setUsername(DBConnection.USERNAME);
rowSet.setPassword(DBConnection.PASSWORD);

rowSet.setCommand("SELECT * FROM empleados");
rowSet.execute();

// Nos vamos al último registro de nuevo, y le subimos el sueldo un 10%
rowSet.last();
rowSet.updateFloat("sueldo", rowSet.getFloat("sueldo") * 1.1f);
rowSet.updateRow();

// Insertamos un nuevo registro
rowSet.moveToInsertRow();
rowSet.updateString("nombre", "Joaquín");
rowSet.updateString("apellidos", "Cañadas Casas");
rowSet.updateDate("fecha_nacimiento", Date.valueOf(LocalDate.of(1970, 5, 18)));
rowSet.updateFloat("sueldo", 1400.0f);
rowSet.insertRow();

//..

36.5 CachedRowSet

Se trata del interfaz base de todos los RowSet de tipo desconectado (FilteredRowSetJoinRowSet y WebRowSet heredan de este interfaz). La principal diferencia es que solo abre la conexión para rellenar el RowSet de datos, y posteriormente, para enviar las modificaciones a la base de datos.

Al inicializarlo, tenemos que indicar que (índices de) columnas forman parte de la clave (primaria). Y para aceptar los cambios, necesitamos una conexión que tenga el modo auto-commit en false.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!
Por favor ingrese su nombre aquí