jueves, 1 de diciembre de 2016

Java 8 Streams 1/2

Hay que ver lo que nos calentamos la cabeza para llamar de otra forma los procesos repetitivos:

  • bucles (for, while, do, if, ....)
  • Iterators, colecciones ...
Ahora aparecen los Stream que no hay que confundir con los IOStream de java.

No voy a acosonsejar ni desaconsejar su uso, sino que me voy de "safari fotográfico" a conocer las nuevas especies que han entrado en el nicho ecológico de Java.

1. Ideas básicas


Parece ser que un stream es como el operador "|" (pipe) de los scripts de linux, donde la información se genera y se transforma, filtra etc y obtenemos algo.

1. El origen de los streams parece ser que es una colección, como por ejemplo un List<>.
2. Para trasformar una colección en un stream le aplicaremos el método stream.
3. Existen unos métodos que se aplican al stream que son de tipo eager que se ejecutan inmediatamente.
4. En cambio los métodos de tipo lazy no se ejecutan si tras ellos no llamos a un método eager.
5. Si un método devuelve un stream entonces es lazy.
6. Si un método devuelve un valor o nada (void), entonces es eager.

2. Operaciones básicas de los streams

collect(toList()) (Eager) Trasforma un stream en un List<>, si quisieramos transformarlo en un Set<> (que descarta los duplicados) sería collect(toSet()), y mas genericamente

List<String> collected = 
  Stream.of("a", "b", "c") 
  .collect(Collectors.toList()); 

y para comprobar que el resultado es correcto:
     
assertEquals(Arrays.asList("a", "b", "c"), collected); 

y más genericamente podriamos hacer (de momento hablaremos mas adelante del operador ::")

stream.clollect(toCollection(TreeSet::new))



map() (Lazy) Trasforma un stream en otro, realizando una "transformación" de sus elementos

List<String> collected = 
  Stream.of("a", "b", "c")
  .map(string -> string.toUpperCase())   
  .collect(Collectors.toList()); 

aquí convertimos a mayúscula los elementos y para comprobar que el resultado es correcto:
     
assertEquals(Arrays.asList("A", "B", "C"), collected); 



filter() (Lazy) Filtramos los elementos de un stream que cumplen una condición.

List<String> collected = 
  Stream.of("a", "1a", "b", "c" ,"2c")
  .filter(string -> isDigit(string.charAt(0)))   
  .collect(toList()); 

aquí solamente dejamos passar los que comienzan por un número y para comprobar que el resultado es correcto:
     
assertEquals(Arrays.asList("1a", "2c"), collected); 


flatMap() (Lazy) Cambia un objeto por un stream y lo incluye en el stream final.

List<String> collected = 
  Stream.of(asList("a", "b"), asList("c", "d"))
  .flatMap(strings -> strings.stream())   
  .collect(toList()); 

aquí solamente dejamos passar los que comienzan por un número y para comprobar que el resultado es correcto:
     
assertEquals(Arrays.asList("a", "b", "c", "d"), collected); 



max() min() (Lazy) Obiene el máximo o mínimo en base a un comparador.

String masLarga = 
  Stream.of("a", "bc","d", "efg")
  .max(Comparator.comparing(string -> strings.length()))   
  .get(); 

aquí recogemos la tira con longitud máxima y para comprobar que el resultado es correcto:
     

assertEquals("efg", masLarga); 





reduce() (Eager) Obtiene un único resultado. Puede ser la generalizacion de los métodos count, min, max ..

int sumador = 
  Stream.of( 1, 2, 3, 4)
  .reduce( 0, (suma, elemento) -> suma + elemento);
  

aquí recogemos la suma de los elementos  y para comprobar que el resultado es correcto:
     

assertEquals(10, sumador); 


4. Expresiones Lambda

Parece ser que una expresión Lambda es un tipo dado de un interface funcional, y en base a los cuales se puede inferir la función. O sea parecen 2 conceptos:

 1. Interfaz funcional : Parece ser que en Java 8 se han definido 5 interfases funcionales que son
               
InterfazArgumentosDevuelve Ejemplo
Predicate<T> T boolean Si se cumple una condición
Consumer<T> T void imprimir un valor
Function<T,R> T R Obtener el nombre de un cliente
Supplier<T> Nada T Factory method
UnaryOperator<T> T T Logical not (!)
BinaryOperator<T,T> (T, T) T Multiplicar 2 números (*)

Según TutorialsPoint, hay bastante más.

2.Inferencia: En base a como se escribe, el compilador es capaz de inferir a que tipo de interfaz funcional nos estamos refiriendo.

También aparece el concepto de Higher-Order Funtions, que es una función toma otra función como argumento o devuelve otra función. Cuando un interfaz funcional se utiliza como parámetro o como resuiltado, entonces hemos creado un función de Higher-Order (En adelante H-O). De hecho todas las funciones que se utilizan en el Stream interfaz son de H-O.

Una de las ventajas de utilizar los Lambdas es que se hace mas clara la declaración de lo que se quiere hacer.

Una cuestión a tener en cuenta es que las Lambda, solamente pueden acceder a variables externas que sean del tipo "final" o sea que no admita cambios de valor, es decir es una constante. A pesar de las desventajas que tenga esto, tiene la ventaja que no tiene efectos colaterales respecto a las variables externas que utiliza ya que no puede modificarlas.

Veamos un ejemplo donde queremos crear una función que muestre por consola si se verifica una condición:

public void mostrar(Supplier<String> mensaje) {
  if (isCondition()){
    System.out.printl(mensaje.get());
  }
}


Supongamos que este método forma parte de una clase llamada Chivato, que ademas tiene una función que construye un mensaje complejo llamada constructorMensaje. Para llamarlo, como ya podemos utilizar funciones tipo Supplier haríamos:

Chivato chivato = new Chivato();
chivato.mostrar (() -> "Mira esto:" + chivato.constructorMensaje());


5. Primitivas

Los Streams trabajan con objetos y no con primitivas. Si queremos trabajar con primitivas, tenemos dos opciones:

  1. Convertir las primitivas a objetos: Tiene la desventaja que los objetos consumen mas memoria que los primitivas
  2. Utilizar streams adaptados a primitivas.

En el segundo caso hay algunas clases y métodos que permiten trabajar con ellas. Veamos algunos de ellos:

mapToInt (): Extrae un valor entero de cada elemento de una colección de objetos. Devuelve un IntStream.

summaryStatistics (): Obtiene una clase tipo IntSummaryStatistics de un stream de primitivas tipo int.

IntSummaryStatistics (): Clase que guarda las estadísticas (max, min, promedio, suma, etc) de un stream de primitivas tipo int

Veamos un ejemplo


public static void mostrarEstaditicas (Personas personas) }
  IntSummaryStatistic est =
    personas.getEdades()
       .mapToInt( persona -> persona.getEdad())
       .summaryStatistics();

  System.out.printf("Max: %d, MIn: %d: Ave: %f, Sum %d",
    est.getMax(),
    est.getMin(),
    est.getAverage(),
    est.getSum());


Análogamente existen otras clases y métodos para LongStream y DoubleStream.


6. Referencias a métodos

Mediante la referencia a métodos  en las Lambda, se pueden hacer las siguientes equivalencias

             
             
Expresión Nueva
Expresión equivalente
Explicación
Persona::getEdad persona.getEdad() Es de la forma Classname::methodName
Persona::new (nombre, edad) -> new Persona(nombre, edad) Constructor de la clase
String[]::new Creación de un array



7. Las Null Pointer Exceptions y los Optional

La interfaz Optional se utiliza para aquellos casos que puede que el elemento no tenga un valor. Imaginemos una clase Persona que tenga de atributos nobre y teléfono. Hay personas como los niños que no tienen teléfono, por tanto este atributo se puede indicar con Otional:

public class Persona {
  private String nombre;
  private Optional<String> telefono

  public Optional<String> getTelefono() {..}
}


Veamos que ventajas tenemos, al aparecer nuevos métodos:

orElse(): Ofrece valor alternativo si no existe el valor

  String telf = persona.getTelefono().orElse("SIN TELEFONO");




orElseThrow()
: lanza una excepción si no existe el valor

  String telf = persona.getTelefono().orElseThrow(IllegalStatementException::new);


8. Otras utilidades de los streams

1. (maxBy, minBy) Obtener un objeto de entre una colección, que tenga un máximo (o mínimo). Supongamos la anterior clase Persona que tenga un atributo List<Personas> llamado hermanos, que sean los hermanos. Queremos obtener aquellas personas que tengan mayor número de hermanos. Podriamos hacerlo así

public Optional<Persona>masHermanos(Stream<Persona> personas) {
  Function<Persona, Long> getNumHermanos = persona > persona.getHermanos().count();
  return personas.collect(maxBy(comparing(getNumHermanos)));
}

igualmente existe minBy.

2.(averagingInt) Obtener el promedio de un valor de una colección.

public double promedioHermanos(List<Persona> personas) {
  return personas.stream() 
    .collect(averagingInt(persona -> persona.getHermanos.size())));
}


3.(partitioningByPartir una colección en dos en base a un criterio. Supongamos que tenemos una función isHijoUnico que indica si la persona no tiene hermanos. Podemos obtener dos listas de personas, una sin hermanos y otra que tienen hermanos.

public Map<Boolean, List<Persona>> conYSinHermanos(Stream<Persona> personas) {
  return personas.collect(partitioningBy(Persona::isHijoUnico));
}


4.(groupingBy) Agrupar varias coleccines en una sola. Supongamos que tenemos un campo en la persona que es la edad en años que es entero, y queremos agrupar todas las personas que tienen la misma edad.


public Map<int, List<Persona>> personasEdad(Stream<Persona> personas) {
  return personas.collect(groupingBy(Persona::getEdad));
}

5.(joining) Agrupar los nombres de las personas en un string separdo por comas, y de corchetes en los extremos.  Ejempo "[David, Juan, Enrique, Eduardo]" 



String
nombres

  personas.stream()
    .map(Persona::getNombre)
    .collect(Collectors.joining(", ", "[", "]"));

6.(countingObtener el numero de personas de una misma edad.

public Map<int, Long> personasEdad(Stream<Persona> personas) {
  return personas.collect(groupingBy(Persona::getEdad, counting()));


}



No hay comentarios :

Publicar un comentario