10.Lambda

Lambda

Introducción a lambda

Introdución a lambda

Las expresiones lambda surgen a partir de la versión 8 de Java, ante la necesidad de “modernizarse” contra otros lenguajes de programación. La idea es incluir programación funcional en Java, crear sentencias más directas o resolver el problema en menos líneas de código (menos verboso). Se busca la simplicidad. En cada nueva versión del jdk, se ha ido incluyendo más expresiones lambda y también cierta evolución. Por tanto no es un nuevo “Java” sino es un plus que debes agregar a tu programación.

icono lambda

Aspectos ya vistos

Expresiones con lambda

Estructura normal de una expresión lambda: (parámetros) -> cuerpo de la expresión

Lista de argumentos Operador Cuerpo
(x,y) -> { x + y }

Cuerpo de la expresión: Más de una sentencia requiere {} Parétesis para argumento más de uno.

Aspecto importante a tener en cuenta es que hay inferencia de tipos de datos, es decir en lo máximo posible no es necesario poner el tipo de dato. Esto implica expresiones más compactas y simples.

Debemos usar interfaces funcionales para usar expresiones lambda. Las más frecuentes entán en el paquete java.util.function.

Video explicativo de la sintáxis: enlace

Interfaces funcionales

Las interfaces funcionales son interfaces que tienen un método a implementar, es decir, un método abstracto. Esto significa que cada interfaz creada que respeta esta premisa se convierte automáticamente en una interfaz funcional. Toda expresión lambda debe convertirse o usar una interfaz funcional.

Interfaz Consumer

enlace

De las más sencillas recibe un argumento (genérico), pero no devuelve nada. Consumer representa una función con un argumento que no retorna nada. Tal y como su nombre indica, consume un valor y no genera nada. Un ejemplo de este tipo de funciones es el método de impresión por la salida estándar, que admite el texto a imprimir y no retorna nada. Ejemplo:

List<Integer> numbers = Arrays.asList(1,3,4,6);
numbers.forEach(number -> System.out.println(number));    

El forEach puede recibir un Consumer como parámetro. Existe la opción Biconsumer, que tiene dos argumentos de entrada.

Interfaz Supplier

enlace

Supplier representa una función sin argumentos que retorna un resultado. Tal y como su nombre indica, genera un valor. Un ejemplo de este tipo de funciones es el método de generación de números aleatoriosde Math, que no requiere argumentos:

Supplier<Integer> randomNumbersSupp=() -> new Random().nextInt(10);
Supplier<LocalDateTime> s = () -> LocalDateTime.now();
LocalDateTime time = s.get();
System.out.println(time);

Interfaz Predicate

enlace

Recibe un argumento y devuelve un booleano. Sirve para comprobar si una condición es verdadera o falsa.

Predicate<String> checker = a -> a.startsWith("M");
System.out.println(checker.test("Miguel"));

Lo utilizaros para aspectos condicionales dentro de una expresión lambda. También existe la versión con dos argumentos Bipredicate.

Interfaz Function

enlace

Recibe un parámetro y devuelve otro parámetro. Hay versiones con más opciones (Bifunction por ejemplo).

package com.arquitecturajava.functional;
 
public class Principal {
@FunctionalInterface
interface Matematicas {
public double operacion(double x, double y);
}
public static void main(String[] args) {
    Matematicas o = (x, y) -> x + y;
    System.out.println(o.operacion(2, 3));
 }
}

Similar a Function pero si queremos trabajar con siempre mismo tipo de datos podemos usar UnaryOperator. Por ejemplo:

UnaryOperator<Integer> multiplicarUnNumeroporDos = x -> x * 2;

Ejercicio 1

Crea una interfaz funcional, para usarla con lambda que devuelva el divisor más pequeño de un valor ( el parámetro de entrada).

Ejercicio 2

Crea una interfaz funcional, para usarla con lambda que recibe un parámetro tipo String y devuelve ese mismo String al revés. Entrada: Hola Mundo Salida: odnuM aloH

Ejercicio 3

Usa la interfaz supplier para “contener” un objeto de la clase:

public class Student {
    private int id;
    private String name;
    private String gender;
    private int age;

Uso de Stream y Colecciones

Api Stream

A partir de Java 8 ha aparecido esta api que facilita el trabajo con colecciones y flujo de datos/trabajo. Con Stream podemos usar todo lo visto con lambda, y de esta manera se exprime todas sus posibilidades.

Podemos obtener el stream de cualquier colección de esta manera:

Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();

Si se necesita trabajar con Array tenemos estas posibilidades:

Stream<String> streamOfArray = Stream.of("a", "b", "c");

String[] arr = new String[]{"a", "b", "c"};
Stream<String> streamOfArrayFull = Arrays.stream(arr);

Stream tiene muchas posibilidades como por ejemplo con String, Ficheros o generar contenido automático (enlace enlace2)

Uso con colecciones

Usarlo con Maps:

Map<String, Integer> map = new HashMap<>();
map.put("Pepe",5);
map.put("Fran", 8);
map.put("Jose", 7);

List<String> biggerThanSix = map.entrySet().stream()
  .filter(e -> 6 < e.getValue())
  .map(a -> a.getKey())
  .collect(Collectors.toList());

El método estático Stream.of puede ser muy útil para crear streams de datos primitivos o objetos, como en este ejemplo:

Stream<Person> userStream = Stream.of(
    new Person("Mahesh", 22),
    new Person("Krishn", 20),
    new Person("Suresh", 25)
);
userStream.forEach(u -> System.out.println(u.getNombre()));
//Uso con Strings
Stream<String> mystream = Stream.of("AA", "BB", "CC", "DD");
mystream.forEach(e -> System.out.println(e));

Operación forEach

La operación más usada, y que ya hemos usado. Implica un bucle que recorre todos los objetos del Stream. ForEach acepta expresiones lambda.

Operación Map

El uso de Java Stream map es una de las operaciones más comunes cuando trabajamos con un flujo de Streams. El método map nos permite realizar una transformación rápida de los datos y muy directa sobre el flujo original. Es decir “mapea” lo que necesitas de cada objeto de la colección:

Persona p1 = new Persona("pedro", 20, "perez");
Persona p2 = new Persona("juan", 25, "perez");
Persona p3 = new Persona("ana", 30, "perez");
List < Persona > lista = new ArrayList < Persona > ();
lista.add(p1);
lista.add(p2);
lista.add(p3);

lista
    .stream()
    .map(a -> a.edad )
    .forEach(a -> System.out.println(a));;

Es muy frecuente usar la operación .sum con el mapeo. Pruébalo!

Operación filter

Como el propio nombre indica filtra a partir de una condición los objetos que queremos, al más puro estilo “where” en una consulta SQL. Esta operación necesita una interfaz funcional Predicate. Por lógica algo que devuelva booleano. Si queremos rapidez en la instrucción lambda se debe colocar de primero.

Libro l = new Libro("El señor de los anillos", "fantasia", 1100);
Libro l2 = new Libro("El Juego de Ender", "ciencia ficcion", 500);
Libro l3 = new Libro("La fundacion", "ciencia ficcion", 500);
Libro l4 = new Libro("Los pilares de la tierra", "historica", 1200);
List <Libro> lista = Arrays.asList(l, l2, l3, l4);
lista.stream()
    .filter(libro -> libro.getPaginas() > 1000)
    .map(libro -> libro.getTitulo())
    .forEach(System.out::println);

Podemos crear predicados muy complejos y luego usarlos con Stream y filter cuando queramos:

public static boolean buenosLibros(Libro libro) {
        Predicate <Libro> p1 = (Libro l) -> l.getCategoria().equals("ciencia ficcion");
        Predicate <Libro> p2 = (Libro l) -> l.getCategoria().equals("fantasia");
        Predicate <Libro> p3 = (Libro l) -> l.getPaginas() > 1000;
        Predicate <Libro> ptotal = p1.or(p2).and(p3);
        return ptotal.test(libro);
    }

Con filter también es común usar count para obtener el número de elementos que cumple la condición:

cars.stream().filter(x -> x.getColor().equals("White")).count();

Operación Collect

Esta operación se utiliza para obtener una nueva colección a partir de operaciones con Stream:

List<Car> result = cars.stream().filter(x -> x.getColor().equals("White")).collect(Collectors.toList());

El anterior ejemplo obtenemos una nueva lista, pero hay opciones para mapas y conjuntos: Collectors.toSet() o Collectors.toMap.

Operación Sorted

También podemos ordenar un Stream, esta operación puede recibir varios tipos de argumentos:

  • Comparator.comparing():
  lista.stream()
    .sorted(Comparator.comparing( p -> p.getApellidos()))
    .forEach(System.out::println);
  • CompareTo con un BiPredicate:
List<Employee> employees = empList.stream()
      .sorted((e1, e2) -> e1.getName().compareTo(e2.getName()))
      .collect(Collectors.toList());

Operación distinct

Genera una lista de objectos descartando los iguales:

List<String> ids = cars.stream()
.filter(x -> x.getColor().equals("White"))
.map(car -> car.getBrand())
.distinct().collect(Collectors.toList());

Si mantenemos los objetos en el stream, la operación distinct usará el método equals de la clase correspondiente, que debemos sobreescribir.

Operación parallel

En el caso que necesitemos mejorar el rendimiento al trabajar con Stream, tenemos la operación parallel que genera varios hilos de ejecución. Tal como vimos con ficheros. Os dejo un enlace para que podáis probarlo: enlace

Las siguientes operaciones es necesario conocer la clase Optional

Operación findFirst

Esta operación nos devolverá el primer elememento del Stream, y los devolverá un objeto de la clase Optional, es decir el primer elemento o sino existe referencia a null.

@Test
public void createStream_whenFindFirstResultIsPresent_thenCorrect() {

    List<String> list = Arrays.asList("A", "B", "C", "D");

    Optional<String> resultFirst = list.stream().findFirst();

   	if (resultFirst.isPresent()){System.out.println(resultFirst.get());}

}

Sino no nos importa el orden y nos vale cualquier elemento podéis probar Stream.findAny().

Operación min max

Estas operaciones nos devolverá el mínimo o el máximo de un Stream a partir de recibir un Comparator (Compare, Comparing) de parámetro. Y nos devolverá un Optional como en la operación anterior. Ejemplos sencillos:

List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
Integer var = list.stream().min(Integer::compare).get();

List<Integer> list = Arrays.asList(-9, -18, 0, 25, 4);
Optional<Integer> var = list.stream()
                    .min(Comparator.reverseOrder());

Ejercicio 1

Partiendo de un array de String, usa Stream y min para obtener el string más pequeño del array:

Ejercicio 2

Crea una lista de objetos tipo Persona, una vez lista la colección crea una instrucción lambda que nos devuelva la persona con menos edad.

Ejercicio 3

Genera una interfaz supplier que genera un valor entero (entre 1 y 10) al azar. Prueba Stream.generate para generar 5 numeros (.limit) y los muestre por pantalla.

Ejercicio 4

Partiendo de este pojo:

public class Factura  {
    private int numero;
    private String concepto;
    private double importe;
}

Implementa gets, sets, equals, hashCode, constructor por parámetros y toString. Crea una colección donde almacenes objetos factura. Usando lambda obtén una nueva colección filtrada con las facturas de importe mayor a 200. Muestra por pantalla el concepto de la factura. También nos piden una nueva colección con las facturas entre 200 y 300 (investiga el and de Predicate).

Operador "::"

Operador “::” también llamado “referencia del método” surgió a partir de Java 8, nos sirve para abreviar expresiones lambda entre otras funcionalidades. Por ejemplo:

Comparator x = (c1, c2) -> ((Computer) c1).getAge().compareTo(((Computer) c2).getAge());
		
Comparator c = Comparator.comparing(Computer::getAge);

//Expresiones identicas
a -> System.out.println(a)
System.out::println

Sintaxis: “::

Como podéis obsevar con este operador accedemos a un método de la clase ya implementado. Este operador ya lo hemos utilizado con ficheros con lambda por ejemplo accediendo a println.

Posibilidades de uso

Acceso a métodos estáticos de una clase

Clases de ejemplo para probar lo siguiente: Computer.java, ComputerUtils.java y OrdenadorHijo.java

Ejemplo:

List <Computer> inventory = Arrays.asList(
  new Computer( 2015, "white", 35), new Computer(2009, "black", 65));
inventory.forEach(ComputerUtils::repair);

Acceso a método normales de un objeto

Computer c1 = new Computer(2015, "white");
Computer c2 = new Computer(2009, "black");
Computer c3 = new Computer(2014, "black");
Arrays.asList(c1, c2, c3).forEach(System.out::print);

Uso con herencia

Método de la superclase:

public Double calculateValue(Double initialValue) {
    return initialValue/1.50;
}

Método sobreescrito en clase hija:

@Override
public Double calculateValue(Double initialValue){
    Function<Double, Double> function = super::calculateValue;
    Double pcValue = function.apply(initialValue);
    return pcValue + (initialValue/10) ;
}

Llamar al constructor

Con la palabra new podemos ejecutar el constructor de una clase:

class GFG {
  
    // Class constructor
    public GFG(String s)
    {
        System.out.println("Hello " + s);
    }
  
    // Driver code
    public static void main(String[] args)
    {
  
        List<String> list = new ArrayList<String>();
        list.add("Geeks");
        list.add("For");
        list.add("GEEKS");
  
        // call the class constructor
        // using double colon operator
        list.forEach(GFG::new);
    }
}

Ejercicio 1

Crea una lista de nombres, ordénalos (por longuitud) y muéstralos por pantalla usándo lambda y el operador ::