Skip to content

4. Expresiones lambda de Java 8

4.1. Ejemplo 01: Interfaces funcionales y lambdas

  

Considera el siguiente código:


package dvp.java8.lambdas;
 
public class Exemple01 {
  public static void main(String[] args) {
 
    // anonymous classes
    I1 ia1 = new I1() {
      @Override
      public void doSomething() {
        System.out.println("ia1.doSomething");
      }
    };
 
    I2 ia2 = new I2() {
      @Override
      public String getSomething(double value) {
        return String.format("ia2.getSomething(%s)", value);
      }
    };
 
    // lambdas
    I1 ib1 = () -> System.out.println("ib1.lambda");
    I2 ib2 = (value) -> String.format("ib2.lambda(%s)", value);
    I1 ib3 = () -> {
      System.out.println("ib3.lambda");
    };
    I2 ib4 = (double value) -> {
      return String.format("ib4.lambda(%s)", value);
    };
 
    // app
    ia1.doSomething();
    System.out.println(ia2.getSomething(4.3));
    ib1.doSomething();
    System.out.println(ib2.getSomething(5.8));
    ib3.doSomething();
    System.out.println(ib4.getSomething(10.1));
  }
}
 
@FunctionalInterface
interface I1 {
  void doSomething();
}
 
@FunctionalInterface
interface I2 {
  String getSomething(double value);
}
  • líneas 41–44: definen una interfaz funcional I1. Una interfaz funcional es una interfaz que solo tiene un método. No depende de la presencia de la anotación [@FunctionalInterface] en la línea 41, que es opcional;
  • líneas 46–49: una segunda interfaz funcional I2;
  • líneas 6–19: las interfaces I1 e I2 se implementan mediante clases anónimas, la solución más habitual antes de la introducción de las funciones lambda;
  • líneas 21–29: las interfaces I1 e I2 se implementan utilizando funciones lambda;
  • líneas 7-12: implementación de la interfaz I1 con una clase anónima. La sintaxis para implementar una interfaz I con una clase anónima es la siguiente:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

donde m1, m2, ... son los métodos definidos por la interfaz I.

  • líneas 14–19: implementación de la interfaz I2 con una clase anónima;
  • línea 22: implementación de la interfaz I1 con una función lambda. Aquí aprovechamos el hecho de que la interfaz funcional solo tiene un método. La función lambda implementa entonces este único método M. Su sintaxis es la siguiente:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Los tipos T1, T2, Tn pueden omitirse si el compilador puede inferirlos a partir del contexto (inferencia de tipos).

  • línea 22: implementación del método [I1.doSomething] con la firma:

void doSomething();

[doSomething] es un método que no tiene parámetros y no devuelve ningún resultado. Su implementación lambda puede escribirse como en la línea 22 o como en las líneas 24–26, es decir, se pueden colocar llaves alrededor del código de la función lambda. Si este código contiene solo una instrucción, como en este caso, estas llaves pueden omitirse;

  • Línea 23: Implementación del método [I1.getSomething] con la siguiente firma:

String getSomething(double value);

[getSomething] toma un parámetro de tipo [double] y devuelve un resultado de tipo [String]. Su implementación lambda puede ser la de la línea 23 o la de las líneas 27–29. En la implementación de la línea 23:

  • se omite el tipo del parámetro [value]. Se utilizará entonces el tipo [double] que se encuentra en la firma de [getSomething];
  • el código lambda no está entre paréntesis. El resultado de la lambda es entonces el valor de la única expresión de ese código, en este caso: String.format("ib2.lambda(%s)", value);

En la implementación de las líneas 27–29:

  • se declara explícitamente el tipo del parámetro [value];
  • utilizamos un [return] para devolver el resultado de la lambda. En este caso, deben utilizarse llaves;
  • Líneas 32-37: llamamos a las diversas funciones anónimas y lambdas;

El resultado obtenido es el siguiente:

1
2
3
4
5
6
ia1.doSomething
ia2.getSomething(4.3)
ib1.lambda
ib2.lambda(5.8)
ib3.lambda
ib4.lambda(10.1)

4.2. Ejemplo-02: la interfaz funcional Predicate<T>

  

La mayoría de las veces, trabajamos con interfaces funcionales de bibliotecas en lugar de interfaces funcionales que definimos nosotros mismos. En este caso, nos interesa la interfaz funcional [Predicate] definida en el paquete [java.util.function], que contiene la mayoría de las interfaces funcionales de Java 8. Se define de la siguiente manera:

Image

Hemos mencionado que una interfaz funcional solo tiene un método. Aquí, sin embargo, hay cuatro. Otra innovación introducida por Java 8 fue el concepto de método predeterminado en una interfaz, marcado por la palabra clave [default]. Aquí tenemos tres métodos de este tipo. Estos métodos tienen la particularidad de contar con una implementación predeterminada. Por lo tanto, no es necesario que una clase que implemente una interfaz con métodos por defecto los implemente. Así, una clase que desee implementar la interfaz [Predicate] solo tiene un método que debe implementar: el método [test]. La interfaz [Predicate] es, por lo tanto, efectivamente una interfaz funcional. Podemos decir, pues, que una interfaz funcional es una interfaz cuya implementación contiene solo un método obligatorio. Si tiene más de un método, los demás deben llevar la palabra clave [default].

El método [Predicate<T>.test] toma un parámetro de tipo T y devuelve un valor booleano. Esta interfaz se utiliza generalmente para filtrar colecciones. Para ilustrar su uso, utilizaremos los siguientes datos:


package dvp.data;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Personne {
 
  public enum Sexe {HOMME,FEMME};
 
  // data
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // manufacturers
  public Personne() {
 
  }
 
  public Personne(String nom, Sexe sexe, int age, double poids) {
    this.nom = nom;
    this.sexe=sexe;
    this.age = age;
    this.poids = poids;
  }
 
  // getters and setters
...
 
  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • líneas 32–38: el método [toString] devuelve la cadena JSON de la persona;

La clase [People] define una lista de 3 personas:


package dvp.data;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
import java.util.Arrays;
import java.util.List;
 
public class Personnes {
  private static List<Personne> personnes = Arrays.asList(new Personne("jean", Personne.Sexe.HOMME, 20, 70),
    new Personne("marie", Personne.Sexe.FEMME, 10, 30), new Personne("camille", Personne.Sexe.FEMME, 30, 55));
 
  public static List<Personne> get() {
    return personnes;
  }
 
  public static String toString(List<Personne> liste) {
    try {
      return new ObjectMapper().writeValueAsString(liste);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • líneas 10–11: la lista de 3 personas;
  • líneas 13–15: un método estático para recuperar esta lista;
  • líneas 17–23: un método estático para obtener la cadena JSON de una lista de personas pasada como parámetro;

Estos datos serán utilizados por el siguiente código:


package dvp.java8.lambdas;
 
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple02 {
  public static void main(String[] args) {
    // predicate implemented by anonymous class
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predicate implemented by a lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // list of persons
    List<Personne> personnes = Personnes.get();
    // displays
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterAge)));
    System.out.println(Personnes.toString(filterPersonnes(personnes, filterPoids)));
  }
 
  private static List<Personne> filterPersonnes(List<Personne> personnes, Predicate<Personne> filter) {
      // [filter] filters the [people] list
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • líneas 13–18: implementación de la interfaz Predicate<Person> utilizando una clase anónima. Se trata de un filtro basado en el peso de la persona;
  • línea 20: implementación de la interfaz Predicate<Person> utilizando una función lambda. Se trata de un filtro basado en la edad de la persona. Según lo dicho anteriormente, también se podría haber escrito de la siguiente manera:

        Predicate<Personne> filterAge = (Personne p) -> {
            return (p.getAge() < 28);
        };

pero la versión de la línea 20 es más concisa. El tipo del parámetro p se deduce del contexto. Aquí estamos construyendo un tipo [Predicate<Person>]. El método implementado tiene entonces la firma [boolean test(Person param)]. Por lo tanto, el tipo implícito de p en la línea 20 es el tipo [Person];

  • línea 22: recuperamos una lista predefinida de personas;
  • línea 24: las filtramos por edad;
  • línea 25: las filtramos por peso. En ambos casos, mostramos la cadena JSON de la lista filtrada;
  • líneas 28–37: un método estático que
    • toma como parámetros: una lista de personas para filtrar y el filtro. El filtro es una instancia de la interfaz [Predicate<Person>]. Por comodidad, nos referimos aquí y en el resto del documento a una instancia de una interfaz I como una instancia de una clase C que implementa I;
    • devuelve la lista filtrada como resultado;
  • línea 32: utilizamos el método [test] de la interfaz [Predicate]. Dependiendo del filtro pasado al método, el método [test] será:

return personne.getPoids()<50;

o


return p.getAge() < 28

Al ejecutar la clase [Exemple02] se obtiene el siguiente resultado:

[{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"},{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]
[{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}]

4.3. Ejemplo-03 - La interfaz funcional Function<T,R>

  

La interfaz funcional Function<T,R> se define de la siguiente manera:

Image

El único método de la interfaz tiene la firma R apply(T t). Se utiliza generalmente para crear un nuevo tipo Collection<R> a partir de un tipo Collection<T>. Para ilustrar esta interfaz, utilizaremos el siguiente código:


package dvp.java8.lambdas;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import dvp.data.Personne;
import dvp.data.Personnes;
 
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
 
public class Exemple03 {
  public static void main(String[] args) throws JsonProcessingException {
    // implementation with anonymous class
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementation with lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // list of persons
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // displays
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }
 
  // transformation List<Person> --> List<T>
  private static <T> List<T> mapPersonnes(List<Personne> personnes, Function<Personne, T> mapper) {
    List<T> maps = new ArrayList<>();
    for (Personne p : personnes) {
      maps.add(mapper.apply(p));
    }
    return maps;
  }
}
  • líneas 15–20: implementamos la interfaz [Function<Person, String>] con una clase anónima que realiza la transformación de Person a String;
  • línea 22: implementamos la interfaz [Function<Person, Integer>] con una función lambda que realiza la transformación de Person a Integer;
  • Líneas 33–39: un método estático que
    • toma dos parámetros: el primero, de tipo List<Person>, es una lista de personas que se van a transformar. El segundo, de tipo Function<Person, T>, es una función que toma cada persona de la lista y crea un objeto de tipo T;
    • devuelve una Lista<T> en la que cada elemento es el resultado de la transformación de Persona a T;
  • líneas 35–37: se aplica la transformación de Persona a T. Si el segundo parámetro del método es el objeto [mapToName], se realiza una transformación de Persona a String. Si es el objeto [mapToAge], se realiza una transformación de Persona a Integer;

El resultado es el siguiente:

["jean","marie","camille"]
[20,10,30]

4.4. Ejemplo-04: la interfaz funcional Consumer<T>

  

La interfaz funcional Consumer<T> se define de la siguiente manera:

Image

El único método de la interfaz tiene la siguiente firma: void accept(T t). Este método procesa (consume) su parámetro y no devuelve ningún resultado. Para ilustrarlo, utilizaremos el siguiente código:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.function.Consumer;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple04 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // immplémentaton lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // displays
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • líneas 14–19: la interfaz [Consumer<Person>] se implementa mediante una clase anónima cuyo método [accept] muestra el nombre y la edad de la persona;
  • línea 21: la interfaz [Consumer<Person>] se implementa mediante una función lambda cuyo método implícito [accept] muestra el nombre y el peso de la persona;
  • líneas 23-25: la lista de personas es consumida por la implementación [consumerAge];
  • líneas 27-29: la lista de personas es consumida por la implementación [consumerWeight];

Los resultados son los siguientes:

1
2
3
4
5
6
7
 age de jean = 20
 age de marie = 10
 age de camille = 30
--------
 poids de jean = 70.0
 poids de marie = 30.0
poids de camille = 55.0

4.5. Ejemplo-05: la interfaz funcional BiConsumer<T,U>

  

La interfaz funcional BiConsumer<T,U> se define de la siguiente manera:

Image

El único método de la interfaz tiene la firma: void accept(T t, U u). Consume el tipo T con la información adicional U u. Ilustraremos su uso con el siguiente código:


package dvp.java8.lambdas;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
import java.util.List;
import java.util.function.BiConsumer;
 
public class Exemple05 {
  public static void main(String[] args) {
    // list of persons
    List<Personne> personnes = Personnes.get();
    // anonymous implementation
    BiConsumer<Personne, Integer> biconsumerAge = new BiConsumer<Personne, Integer>() {
      @Override
      public void accept(Personne personne, Integer integer) {
        personne.setAge(personne.getAge() + integer);
        System.out.printf("age de %s = %s%n", personne.getNom(), personne.getAge());
      }
    };
    // lambda implementation
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // displays
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • líneas 14–20: implementación de la interfaz BiConsumer<T,U> utilizando una clase anónima. El método [apply] utiliza su segundo parámetro para actualizar la edad de la persona pasada como primer parámetro. A continuación, muestra el resultado;
  • líneas 22–25: implementación de la interfaz BiConsumer<T,U> utilizando una función lambda. El método implícito [apply] utiliza su segundo parámetro para actualizar el peso de la persona pasada como primer parámetro. A continuación, muestra el resultado;
  • líneas 27-29: la lista de personas se consume utilizando la implementación [biconsumerAge];
  • líneas 31-33: la lista de personas se consume utilizando la implementación [biconsumerWeight];

Los resultados obtenidos son los siguientes:

1
2
3
4
5
6
7
age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

4.6. Ejemplo-06 - La interfaz funcional BiFunction<T,U,R>

  

La interfaz funcional BiFunction<T,U,R> se define de la siguiente manera:

Image

El único método de la interfaz tiene la siguiente firma: R apply(T t, U u). Este método es similar al método [BiConsumer.apply], pero mientras que este último no devuelve ningún resultado, el método [BiFunction.apply] sí lo hace. Ilustraremos su uso con el siguiente código:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.function.BiFunction;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple06 {
    public static void main(String[] args) {
        // list of persons
        List<Personne> personnes = Personnes.get();
        // anonymous implementation
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // lambda implementation
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // displays
        for (Personne p : personnes) {
            System.out.printf("age de %s = %s%n", p.getNom(), biFunctionAge.apply(p, 100));
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            System.out.printf("poids de %s = %s%n", p.getNom(), biFunctionPoids.apply(p, 200));
        }
    }
}
  • líneas 14–19: La interfaz BiFunction<Person, Integer, Integer> se implementa mediante una clase anónima. Su método [apply] devuelve la edad de la persona pasada como primer parámetro, incrementada en el valor del segundo parámetro;
  • líneas 21-23: La interfaz BiFunction<Person, Integer, Double> se implementa mediante una función lambda. El método [apply] devuelve el peso de la persona pasada como primer parámetro, incrementado en el valor del segundo parámetro;
  • líneas 25-27: la implementación de [biFunctionAge] se aplica a las personas;
  • líneas 29-31: la implementación de [biFunctionWeight] se aplica a las personas;

Los resultados obtenidos son los siguientes:

age de jean = 120
age de marie = 110
age de camille = 130
--------
poids de jean = 270.0
poids de marie = 230.0
poids de camille = 255.0

Además de las funciones lambda, Java 8 introdujo el tipo Stream<T>, que modela un flujo de elementos de tipo T. Estos elementos pueden someterse a transformaciones sucesivas implementadas por funciones lambda. Cuando es posible, y cuando hay varios procesadores, estas transformaciones pueden realizarse en paralelo.

4.7. Ejemplo-07: la interfaz funcional Supplier<T>

  

La interfaz funcional Supplier<T> se define de la siguiente manera:

Image

El único método de la interfaz tiene la firma: T get(), cuya función es devolver un objeto de tipo T.

Ilustraremos esta interfaz funcional con el siguiente código:


package dvp.java8.lambdas;
 
import java.util.List;
import java.util.Random;
import java.util.function.Supplier;
 
import dvp.data.Personne;
import dvp.data.Personnes;
 
public class Exemple07 {
    public static void main(String[] args) {
        // anonymous implementation
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // list of persons
            List<Personne> personnes = Personnes.get();
 
            // interface implementation
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // operation
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }
 
    // person display
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • líneas 13–28: implementación de un tipo Supplier<Person>;
  • líneas 31–33: el método estático [display] espera un parámetro de tipo Supplier<Person>;
  • líneas 25–27: uso de la instancia Supplier<Person>;

Se obtienen los siguientes resultados:

1
2
3
4
5
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}
{"nom":"marie","age":10,"poids":30.0,"sexe":"FEMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"jean","age":20,"poids":70.0,"sexe":"HOMME"}
{"nom":"camille","age":30,"poids":55.0,"sexe":"FEMME"}