Uso básico de Java 8: Stream y Lambdas.

2
111642

En este tutorial vamos a ver ejemplos hechos con Programación imperativa vs. Programación funcional y qué ventajas nos aporta.

Índice de contenidos

1. Introducción

Esta es la primera parte de un tutorial dedicado a Java 8. En esta primera parte elaboraré ejemplos entre Programación imperativa y Programación funcional para en las próximas partes realizar microbenchmarking a mano y con JMH, comparando el rendimiento de cada uno de los ejemplos.

Java 8 nos abre la puerta a la programación funcional con las expresiones lambda, también llamadas funciones anónimas, y la API Stream. Este tutorial no pretende entrar en profundidad en el tema
si no ser un pequeño acercamiento a estas nuevas funcionalidades comparadas con la forma de realizarlas en programación imperativa. Para entrar más en profundidad, tenéis los siguientes
tutoriales:

Si quieres conocer todas las novedades de java 8, pincha en el siguiente enlace: Novedades Java8

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 17′ (3 Ghz Intel Core 2 Duo, 8GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11
  • Entorno de desarrollo: IntelliJ Idea 2016.2
  • JavaSE build 1.8.0_77-b03
  • Maven 3.3.9

3. Proyecto

Como el objetivo de este tutorial es centrarnos en el uso de Stream y Lambdas, he creado el proyecto más sencillo que se me ha ocurrido. Un carrito de la compra que nos devuelve el número de productos
y el precio total de ellos. A continuación dejo el código:

CarritoDeLaCompra.java
import java.util.Collection;

public class CarritoDeLaCompra {

    private Collection precios;

    public CarritoDeLaCompra(Collection precios) {

        this.precios = precios;
    }

    public int calcularPrecioTotal() {

        int precioTotal = 0;

        for(Integer precio : precios){

            precioTotal += precio;

        }
        return precioTotal;
    }

    public int contarNumeroProductos() {

        return precios.size();
    }
}
CarritoBuilder.java
import java.util.ArrayList;

public class CarritoBuilder {

    ArrayList precios = new ArrayList();

    public CarritoBuilder(int size){

        for(int i = 0; i < size; i++){

            Double random = Math.random()*100+1;
            precios.add(random.intValue());
        }
    }

    public CarritoBuilder(int size, int value){

        for(int i = 0; i < size; i++){

            precios.add(value);
        }
    }

    public CarritoDeLaCompra build(){

        return new CarritoDeLaCompra(this.precios);
    }

    public CarritoBuilder add(Integer nuevoValor){

        precios.add(nuevoValor);
        return this;
    }
}

También, como buenos seguidores de la filosofía TDD que somos en Autentia, he creado los test que me permitan probar que lo que realizo es totalmente funcional.
Os dejo también el código de los test:

CarritoDeLaCompraTest.java
import com.autentia.CarritoBuilder;
import com.autentia.CarritoDeLaCompra;
import org.junit.Assert;
import org.junit.Test;

public class CarritoDeLaCompraTest {

    @Test
    public void shouldReturnTheCountOfAllItems() throws Exception {

        CarritoBuilder builder = new CarritoBuilder(30);
        CarritoDeLaCompra carritoDeLaCompra = builder.build();
        Assert.assertEquals(30, carritoDeLaCompra.contarNumeroProductos());
    }

    @Test
    public void shouldCalculateTotalPrice() throws Exception {

        CarritoBuilder builder = new CarritoBuilder(60,5);
        CarritoDeLaCompra carritoDeLaCompra = builder.build();
        Assert.assertEquals(300, carritoDeLaCompra.calcularPrecioTotal());

    }
}

Con el objetivo de en los siguientes tutoriales realizar pruebas de rendimiento, en vez de modificar los métodos voy a crear nuevos. AVISO: los tiempos de ejecución de los test que aparecen en esta primera parte no son representativos.

4. Lambdas y Stream

Empecemos con nuestras primeras líneas de programación funcional. Para empezar, creamos un nuevo método, en mi caso el método calcularPrecioTotalLambda() cuyo código es el siguiente:

calcularPrecioTotalLambda()
public int calcularPrecioTotalLambda() {
    int precioTotal = this.precios.stream().mapToInt(precio -> precio.intValue()).sum();
    return precioTotal;
}

Como podéis observar, hacemos uso del stream, el cual nos recogerá cada objeto de la Collection precios (this.precios.stream()) y lo mapeará a Integers (mapToInt()).
La lambda que pasamos por parámetro a mapToInt nos dará un warning.


Captura de pantalla 2016-05-10 a las 15.26.05

Si pulsamos el atajo [Alt + Enter] la solución será sustituir la lambda por una referencia a método. De tal manera que el código quedaría así:

calcularPrecioTotalRefMethod()
public int calcularPrecioTotalLambda() {
    int precioTotal = this.precios.stream()
                          .mapToInt(Integer::intValue)
                          .sum();
    return precioTotal;
}

Las dos formas hacen lo mismo: sacar de cada Integer su valor. La diferencia es la llamada que hacemos. En el primer caso (i -> i.intValue()) llamamos al método intValue de cada Integer.
En el segundo caso (Integer::intValue) hacemos uso del método a través de una referencia (incluidas en Java 8). Si tuviera que decantarme por una forma, quizás la más clara sea
i -> i.intValue(), pero sin duda la más correcta es Integer::intValue.

Ahora creamos los test de estos dos nuevos métodos y vemos que todo funciona:

CarritoDeLaCompraTest.java
@Test
public void shouldCalculateTotalPriceLambda() throws Exception {

   CarritoBuilder builder = new CarritoBuilder(60, 5);
   CarritoDeLaCompra carritoDeLaCompra = builder.build();
   Assert.assertEquals(300, carritoDeLaCompra.calcularPrecioTotalLambda());
}

@Test
public void shouldCalculateTotalPriceRefMethod() throws Exception {

   CarritoBuilder builder = new CarritoBuilder(60,5);
   CarritoDeLaCompra carritoDeLaCompra = builder.build();
   Assert.assertEquals(300, carritoDeLaCompra.calcularPrecioTotalRefMethod());
}

Captura de pantalla 2016-05-13 a las 15.56.07

5. Filter

Hasta aquí ha sido un primer contacto muy básico con los Streams, las Lambdas y las Referencias a métodos. Pretendía dar a entender las similitudes entre un bucle for y un stream para ahora
entrar más en detalle en las posibilidades que nos ofrece Java 8.

Obviamente con las opciones que tenemos ahora en CarritoDeLaCompra poco podríamos hacer, por lo que, debemos extender funcionalidades. La primera va a ser un detector de descuentos
que comprueba si algún precio es mayor o igual que el que pasamos por parámetro y, por cada coincidencia, descuenta un 5% :

calcularDescuentoTotal()
public long calcularDescuentoTotal(int cantidadConDescuento){

    long descuentoTotal = 0;

    for(Integer precio : precios){
        if(precio >= cantidadConDescuento){
            descuentoTotal += (cantidadConDescuento*5)/100;
        }
    }
    return descuentoTotal;
}

El método de testeo para este nuevo método:

Test
@Test
public void shouldCalculateTotalDiscount() throws Exception {

		CarritoBuilder builder = new CarritoBuilder(20,100);
		CarritoDeLaCompra carritoDeLaCompra = builder.build();
		Assert.assertEquals(100, carritoDeLaCompra.calcularDescuentoTotal(100));

}


Captura de pantalla 2016-05-12 a las 19.17.38

Ahora, vamos a hacer un nuevo método que use Stream y Lambda:

calcularDescuentoTotalLambda()
public long calcularDescuentoTotalLambda(int cantidadConDescuento){

    long descuentoTotal = 0;

    Long numeroDeDescuentos  = this.precios.stream()
                                           .filter(precio -> precio.intValue() >= cantidadConDescuento)
                                           .count();

    descuentoTotal = (cantidadConDescuento*5/100)*numeroDeDescuentos;

    return descuentoTotal;
}

Volvemos a tener un ejemplo para comparar entre prog. imperativa y prog. funcional, ahora tratemos de entenderlo. Como podéis observar, he añadido un nuevo método llamado filter().
Dicho método recibe como parámetro un predicado. Para entenderlo, no hay más que traducirlo
al español: filtro.

Nos va a servir de filtro con la condición que nosotros le pongamos, aplicando el siguiente método únicamente sobre los valores que la cumplan. En mi caso, la
condición es que cada valor debe ser mayor o igual a la cantidad pasada por parámetro para que se cuente. Al final, tendremos la cuenta de todos los precios que tienen descuento.
Una vez contados aplicamos unas matemáticas básicas para sacar el descuento total.

Ahora creamos el test y lo ejecutamos para comprobar si es correcto.

shouldCalculateTotalDiscountLambda()
@Test
public void shouldCalculateTotalDiscountLambda() throws Exception {

    CarritoBuilder builder = new CarritoBuilder(20,100);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    Assert.assertEquals(100, carritoDeLaCompra.calcularDescuentoTotalLambda(100));

}


Captura de pantalla 2016-05-12 a las 19.38.17

6. anyMatch()

Para no repetir números muy extensos, he creado dos constantes que usaré a lo largo de los próximos test.

    private final Long TOTAL_SIZE = 20000000L;
    private final Long NUMBER_ADD = 1000000L;

Tambien he añadido un método a CarritoBuilder llamado addMultiple() que me permita añadir mucho valores para dejar el número negativo en mitad del array completo.

addMultiple()
public CarritoBuilder addMultiple(int size, int value){

    for(int i = 0; i < size; i++){

        precios.add(value);
    }

    return this;
}

Vamos a seguir añadiendo funcionalidades a nuestro carrito. El nuevo método que he creado si detecta un valor erróneo devuelve true. Los valores erróneos serán
todos aquellos valores que lleguen como un número negativo. A continuación muestro el código del nuevo método:

detectarError()
public boolean detectarError() {

  boolean negativeFind = false;

  for (Long precio : precios) {

     if (precio < 0) {

         negativeFind = true;
     }
  }  

  return negativeFind;
}

Como es evidente, ahora necesitamos la clase de test para este método:

shouldDetectErrorAndReturnTrueWhenAPriceIsNegative()
@Test
public void shouldDetectErrorAnThrowRuntimeExceptionWhenAPriceIsNegative(){

	CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
	builder.add(-1L);
        builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
	CarritoDeLaCompra carritoDeLaCompra = builder.build();
	Assert.assertTrue(carritoDeLaCompra.detectarError());

}

Captura de pantalla 2016-05-24 a las 9.48.40

A continuación dejo el código del ejemplo usando un nuevo método: anyMatch()

detectarErrorAnyMatch()
public boolean detectarErrorAnyMatch() {

    return this.precios.stream().anyMatch(precio -> precio.intValue() < 0);
}

anyMatch(), al igual que filter(), recibe un predicado. En este caso devuelve true si encuentra algún precio negativo.

Creamos el test y lo ejecutamos para comprobar que funciona correctamente:

shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeAnyMatch()
@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeAnyMatch(){

     CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
     builder.add(-1L);
     builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
     CarritoDeLaCompra carritoDeLaCompra = builder.build();
     Assert.assertTrue(carritoDeLaCompra.detectarErrorAnyMatch());

}

Captura de pantalla 2016-05-24 a las 9.52.51

7. findAny() e isPresent()

Para estos nuevos métodos voy a usar el mismo ejemplo de antes. Más adelante veremos si realmente existen diferencias más allá de la sintaxis de cada forma. Tomamos como referencia
el detectarError() y realizamos el siguiente método:

public boolean detectarErrorFindAny() {

   return this.precios.stream().filter(precio -> precio.intValue() < 0)
                               .findAny()
                               .isPresent();
}

En este ejemplo hacemos uso de los métodos findAny() e isPresent(). Si traducimos al español quedaría como "...y encuentra alguno. ¿Está presente?". Es decir, findAny() nos devuelve
un Optional cuando se cumple la condición de filter(). Este Optional tiene como método isPresent() el cual si encuentra una coincidencia devolverá true. Hay que aclarar que esta forma recorre todo el stream.

Habría una forma de evitar que recorra todo el stream y es usando el método findFirst(), es decir, "encuentra el primero".

shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindAny()
@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindAny(){

    CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
    builder.add(-1L);
    builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    Assert.assertTrue(carritoDeLaCompra.detectarErrorFindAny());

}
shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindFirst()
@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindFirst(){

    CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
    builder.add(-1L);
    builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    Assert.assertTrue(carritoDeLaCompra.detectarErrorFindFirst());

}

Ejecutamos los test para comprobar su correcto funcionamiento.

Captura de pantalla 2016-05-24 a las 10.51.25

Existen muchas otras funcionalidades pero el objetivo de este post no es explicarlas todas.

8. parallelStreams()

En este último punto, voy a hacer uso de parallelStreams. Para conocer los parallelStream os recomiendo dos enlaces:

Cogiendo los tres ejemplos de detectar precios nulos, he creado los siguientes métodos usando un parallelStream:

detectarErrorXXXParallel()
public boolean detectarErrorAnyMatchParallel() {
    return this.precios.parallelStream().anyMatch(precio -> precio.intValue() < 0);
}

public boolean detectarErrorFindAnyParallel() {
    return this.precios.parallelStream().filter(precio -> precio.intValue() < 0)
                                        .findAny()
                                        .isPresent();
}

public boolean detectarErrorFindFirstParallel() {

    return this.precios.parallelStream().filter(precio -> precio.intValue() < 0)
                                        .findFirst()
                                        .isPresent();
}
shouldDetectErrorAnThrowRuntimeExceptionWhenAPriceIsNegativeXXXParallel()
@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeAnyMatchParallel(){

    CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
    builder.add(-1L);
    builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    carritoDeLaCompra.detectarErrorAnyMatchParallel();

}

@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindAnyParallel(){

    CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
    builder.add(-1L);
    builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    Assert.assertTrue(carritoDeLaCompra.detectarErrorFindAnyParallel());

}

@Test
public void shouldDetectErrorAndReturnTrueWhenAPriceIsNegativeNumberFindFirstParallel(){

    CarritoBuilder builder = new CarritoBuilder(TOTAL_SIZE,NUMBER_ADD);
    builder.add(-1L);
    builder.addMultiple(TOTAL_SIZE,NUMBER_ADD);
    CarritoDeLaCompra carritoDeLaCompra = builder.build();
    Assert.assertTrue(carritoDeLaCompra.detectarErrorFindFirstParallel());

}

Lanzamos los test por separado:

Captura de pantalla 2016-05-24 a las 10.56.15

Captura de pantalla 2016-05-24 a las 10.56.48

Captura de pantalla 2016-05-24 a las 10.57.15

9. Conclusión

Este tutorial no lo he enfocado en aprender con exactitud todos los entresijos de Java 8, sino más bien en servir de primer contacto a todos aquellos que quieran empezar a usar la programación funcional. Lo cierto es que hay otros lenguajes como Scala que ofrecen una programación funcional bastante más avanzada, pero como dije al principio, Java8 solo ha sido la primera puerta hacia la programación funcional.

En la segunda parte de este tutorial realizaré las pruebas de Benchmarking comparando el rendimiento de cada uno de los métodos que he creado para comprobar en diferentes situaciones cuales son las ventajas y desventajas de la Programación imperativa vs Programación funcional.

2 COMENTARIOS

  1. […] es la segunda parte del tutorial sobre Java 8 que he realizado. En esta segunda parte me centraré en comprobar cuales son las ventajas y […]

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad