Skip to content

4. Espressioni lambda in Java 8

4.1. Esempio-01 - Interfacce funzionali e lambda

  

Si consideri il seguente codice:


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);
}
  • righe 41–44: definiscono un'interfaccia funzionale I1. Un'interfaccia funzionale è un'interfaccia con un solo metodo. Non dipende dalla presenza dell'annotazione [@FunctionalInterface] alla riga 41, che è facoltativa;
  • righe 46–49: una seconda interfaccia funzionale I2;
  • righe 6–19: le interfacce I1 e I2 sono implementate utilizzando classi anonime, la soluzione più comune prima dell'introduzione delle funzioni lambda;
  • righe 21–29: le interfacce I1 e I2 sono implementate utilizzando funzioni lambda;
  • righe 7–12: implementazione dell'interfaccia I1 con una classe anonima. La sintassi per implementare un'interfaccia I con una classe anonima è la seguente:
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

dove m1, m2, ... sono i metodi definiti dall'interfaccia I.

  • righe 14–19: implementazione dell'interfaccia I2 con una classe anonima;
  • riga 22: implementazione dell'interfaccia I1 con una funzione lambda. Qui, sfruttiamo il fatto che l'interfaccia funzionale ha un solo metodo. La funzione lambda implementa quindi questo unico metodo M. La sua sintassi è la seguente:
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

I tipi T1, T2, Tn possono essere omessi se il compilatore è in grado di dedurli dal contesto (inferenza di tipo).

  • riga 22: implementazione del metodo [I1.doSomething] con firma:

void doSomething();

[doSomething] è un metodo che non ha parametri e non restituisce alcun risultato. La sua implementazione lambda può essere scritta come nella riga 22 o come nelle righe 24–26, ovvero è possibile racchiudere il codice della funzione lambda tra parentesi graffe. Se questo codice contiene una sola istruzione, come in questo caso, le parentesi graffe possono essere omesse;

  • Riga 23: Implementazione del metodo [I1.getSomething] con la seguente firma:

String getSomething(double value);

[getSomething] accetta un parametro di tipo [double] e restituisce un risultato di tipo [String]. La sua implementazione lambda può essere quella alla riga 23 o quella alle righe 27–29. Nell'implementazione alla riga 23:

  • il tipo del parametro [value] è omesso. Verrà quindi utilizzato il tipo [double] presente nella firma di [getSomething];
  • il codice lambda non è racchiuso tra parentesi. Il risultato del lambda è quindi il valore della singola espressione in quel codice, in questo caso: String.format("ib2.lambda(%s)", value);

Nell'implementazione delle righe 27–29:

  • il tipo del parametro [value] è dichiarato esplicitamente;
  • utilizziamo un [return] per restituire il risultato della lambda. In questo caso, è necessario utilizzare le parentesi graffe;
  • Righe 32–37: chiamiamo le varie funzioni anonime e lambda;

Il risultato ottenuto è il seguente:

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. Esempio-02 - L'interfaccia funzionale Predicate<T>

  

Il più delle volte abbiamo a che fare con interfacce funzionali provenienti da librerie piuttosto che con interfacce funzionali definite da noi stessi. In questo caso, ci interessa l'interfaccia funzionale [Predicate] definita nel pacchetto [java.util.function], che contiene la maggior parte delle interfacce funzionali di Java 8. È definita come segue:

Image

Abbiamo detto che un'interfaccia funzionale ha un solo metodo. Qui, tuttavia, ce ne sono quattro. Un'altra innovazione introdotta da Java 8 è stata il concetto di metodo predefinito in un'interfaccia, contrassegnato dalla parola chiave [default]. Qui abbiamo tre metodi di questo tipo. Questi metodi hanno la particolarità di avere un'implementazione predefinita. Non è quindi necessario che una classe che implementa un'interfaccia con metodi predefiniti li implementi. Pertanto, una classe che desidera implementare l'interfaccia [Predicate] ha un solo metodo che deve implementare: il metodo [test]. L'interfaccia [Predicate] è quindi effettivamente un'interfaccia funzionale. Possiamo quindi affermare che un'interfaccia funzionale è un'interfaccia la cui implementazione contiene un solo metodo obbligatorio. Se ha più di un metodo, gli altri devono avere la parola chiave [default].

Il metodo [Predicate<T>.test] accetta un parametro di tipo T e restituisce un valore booleano. Questa interfaccia viene generalmente utilizzata per filtrare le collezioni. Per illustrarne l'uso, utilizzeremo i seguenti dati:


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();
    }
  }
}
  • righe 32–38: il metodo [toString] restituisce la stringa JSON della persona;

La classe [People] definisce un elenco di 3 persone:


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();
    }
  }
}
  • righe 10–11: l'elenco di 3 persone;
  • righe 13–15: un metodo statico per recuperare questo elenco;
  • righe 17–23: un metodo statico per ottenere la stringa JSON di un elenco di persone passato come parametro;

Questi dati saranno utilizzati dal codice seguente:


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;  }
}
  • righe 13–18: implementazione dell'interfaccia Predicate<Person> utilizzando una classe anonima. Si tratta di un filtro basato sul peso della persona;
  • riga 20: implementazione dell'interfaccia Predicate<Person> utilizzando una funzione lambda. Si tratta di un filtro basato sull'età della persona. In base a quanto detto, avrebbe potuto essere scritto anche come segue:

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

ma la versione alla riga 20 è più concisa. Il tipo del parametro p viene dedotto dal contesto. Qui stiamo costruendo un tipo [Predicate<Person>]. Il metodo implementato ha quindi la firma [boolean test(Person param)]. Pertanto, il tipo implicito di p alla riga 20 è il tipo [Person];

  • riga 22: recuperiamo un elenco predefinito di persone;
  • riga 24: le filtriamo per età;
  • riga 25: le filtriamo in base al peso. In entrambi i casi, visualizziamo la stringa JSON dell'elenco filtrato;
  • righe 28–37: un metodo statico che
    • accetta come parametri: un elenco di persone da filtrare e il filtro. Il filtro è un'istanza dell'interfaccia [Predicate<Person>]. Per comodità, qui e altrove nel documento ci riferiamo a un'istanza di un'interfaccia I come a un'istanza di una classe C che implementa I;
    • restituisce l'elenco filtrato come risultato;
  • riga 32: utilizziamo il metodo [test] dell'interfaccia [Predicate]. A seconda del filtro passato al metodo, il metodo [test] sarà:

return personne.getPoids()<50;

oppure


return p.getAge() < 28

L'esecuzione della classe [Exemple02] produce il seguente risultato:

[{"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. Esempio-03 - L'interfaccia funzionale Function<T,R>

  

L'interfaccia funzionale Function<T,R> è definita come segue:

Image

L'unico metodo dell'interfaccia ha la firma R apply(T t). Viene generalmente utilizzato per creare un nuovo tipo Collection<R> a partire da un tipo Collection<T>. Per illustrare questa interfaccia, useremo il seguente codice:


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;
  }
}
  • righe 15–20: implementiamo l'interfaccia [Function<Person, String>] con una classe anonima che esegue la trasformazione da Person a String;
  • riga 22: implementiamo l'interfaccia [Function<Person, Integer>] con una funzione lambda che esegue la trasformazione da Person a Integer;
  • Righe 33–39: un metodo statico che
    • accetta due parametri: il primo, di tipo List<Person>, è un elenco di persone da trasformare. Il secondo, di tipo Function<Person, T>, è una funzione che prende ogni persona dall'elenco e crea un oggetto di tipo T;
    • restituisce una List<T> in cui ogni elemento è il risultato della trasformazione da Person a T;
  • righe 35–37: viene applicata la trasformazione Person -> T. Se il secondo parametro del metodo è l'oggetto [mapToName], viene eseguita una trasformazione Person -> String. Se è l'oggetto [mapToAge], viene eseguita una trasformazione Person -> Integer;

Il risultato è il seguente:

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

4.4. Esempio-04 - L'interfaccia funzionale Consumer<T>

  

L'interfaccia funzionale Consumer<T> è definita come segue:

Image

L'unico metodo dell'interfaccia ha la firma: void accept(T t). Questo metodo elabora (consuma) il proprio parametro e non restituisce alcun risultato. Per illustrare questo concetto, utilizzeremo il seguente codice:


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);
        }
    }
}
  • righe 14–19: l'interfaccia [Consumer<Person>] è implementata da una classe anonima il cui metodo [accept] visualizza il nome e l'età della persona;
  • riga 21: l'interfaccia [Consumer<Person>] è implementata da una funzione lambda il cui metodo implicito [accept] visualizza il nome e il peso della persona;
  • righe 23–25: l'elenco delle persone viene consumato dall'implementazione [consumerAge];
  • righe 27–29: l'elenco delle persone viene utilizzato dall'implementazione [consumerWeight];

I risultati sono i seguenti:

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. Esempio-05 - L'interfaccia funzionale BiConsumer<T,U>

  

L'interfaccia funzionale BiConsumer<T,U> è definita come segue:

Image

L'unico metodo dell'interfaccia ha la firma: void accept(T t, U u). Consuma il tipo T con le informazioni aggiuntive U u. Ne illustreremo l'uso con il codice seguente:


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);
    }
  }
}
  • righe 14–20: implementazione dell'interfaccia BiConsumer<T,U> utilizzando una classe anonima. Il metodo [apply] utilizza il suo secondo parametro per aggiornare l'età della persona passata come primo parametro. Quindi visualizza il risultato;
  • righe 22–25: implementazione dell'interfaccia BiConsumer<T,U> utilizzando una funzione lambda. Il metodo implicito [apply] utilizza il suo secondo parametro per aggiornare il peso della persona passata come primo parametro. Quindi visualizza il risultato;
  • righe 27–29: l'elenco delle persone viene consumato utilizzando l'implementazione [biconsumerAge];
  • righe 31–33: l'elenco delle persone viene elaborato utilizzando l'implementazione [biconsumerWeight];

I risultati ottenuti sono i seguenti:

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. Esempio-06 - L'interfaccia funzionale BiFunction<T,U,R>

  

L'interfaccia funzionale BiFunction<T,U,R> è definita come segue:

Image

L'unico metodo dell'interfaccia ha la firma: R apply(T t, U u). Questo metodo è simile al metodo [BiConsumer.apply], ma mentre quest'ultimo non restituisce un risultato, il metodo [BiFunction.apply] restituisce invece un risultato. Ne illustreremo l'uso con il codice seguente:


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));
        }
    }
}
  • righe 14–19: L'interfaccia BiFunction<Person, Integer, Integer> è implementata utilizzando una classe anonima. Il suo metodo [apply] restituisce l'età della persona passata come primo parametro, aumentata del valore del secondo parametro;
  • righe 21–23: l'interfaccia BiFunction<Person, Integer, Double> viene implementata utilizzando una funzione lambda. Il metodo [apply] restituisce il peso della persona passata come primo parametro, aumentato del valore del secondo parametro;
  • righe 25–27: l'implementazione [biFunctionAge] viene applicata alle persone;
  • righe 29–31: l'implementazione [biFunctionWeight] viene applicata alle persone;

I risultati ottenuti sono i seguenti:

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

Oltre alle funzioni lambda, Java 8 ha introdotto il tipo Stream<T>, che modella un flusso di elementi di tipo T. Questi elementi possono subire trasformazioni successive implementate da funzioni lambda. Quando possibile, e in presenza di più processori, queste trasformazioni possono talvolta essere eseguite in parallelo.

4.7. Esempio-07 - L'interfaccia funzionale Supplier<T>

  

L'interfaccia funzionale Supplier<T> è definita come segue:

Image

L'unico metodo dell'interfaccia ha la firma: T get(), il cui ruolo è quello di restituire un oggetto di tipo T.

Illustreremo questa interfaccia funzionale con il seguente codice:


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());
    }
}
  • righe 13–28: implementazione di un tipo Supplier<Person>;
  • righe 31–33: il metodo statico [display] richiede un parametro di tipo Supplier<Person>;
  • righe 25–27: utilizzo dell'istanza Supplier<Person>;

Si ottengono i seguenti risultati:

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"}