Skip to content

4. Le funzioni lambda di Java 8

4.1. Esempio-01 - Interfacce funzionali e lambda

  

Consideriamo il seguente codice:


package dvp.java8.lambdas;

public class Exemple01 {
  public static void main(String[] args) {

    // classi anonime
    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);
      }
    };

    // lambda
    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 che ha un solo metodo. Non è legata alla 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 con classi anonime, la soluzione più comune prima dell’introduzione delle funzioni lambda;
  • righe 21-29: le interfacce I1 e I2 sono implementate con 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. In questo caso si sfrutta il fatto che l'interfaccia funzionale abbia 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 la seguente 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 oppure come nelle righe 24-26, ovvero è possibile racchiudere il codice della funzione lambda tra parentesi graffe. Se tale 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 della riga 23 o quella delle righe 27-29. Nell'implementazione della riga 23:

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

Nell’implementazione delle righe 27-29:

  • si dichiara esplicitamente il tipo del parametro [value];
  • Si utilizza un [return] per restituire il risultato della funzione lambda. In questo caso, è necessario inserire le parentesi graffe;
  • righe 32-37: si chiamano 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>

  

Nella maggior parte dei casi, abbiamo a che fare con interfacce funzionali di 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 raggruppa la maggior parte delle interfacce funzionali di Java 8. Questa è definita come segue:

Image

Abbiamo detto che un’interfaccia funzionale ha un solo metodo. In questo caso, invece, ce ne sono quattro. Un’altra innovazione introdotta da Java 8 è stata quella di introdurre il concetto di metodo predefinito di un’interfaccia, caratterizzato dalla parola chiave [default]. In questo caso abbiamo tre metodi di questo tipo. Questi metodi hanno la particolarità di avere un’implementazione predefinita. Non vi è quindi alcun obbligo per una classe che implementi un'interfaccia con metodi predefiniti di implementare questi ultimi. Pertanto, una classe che voglia implementare l'interfaccia [Predicate] ha un solo metodo da implementare obbligatoriamente: il metodo [test]. L’interfaccia [Predicate] è quindi effettivamente un’interfaccia funzionale. Si dirà quindi che un’interfaccia funzionale è un’interfaccia la cui implementazione comprende un solo metodo obbligatorio. Se presenta più di un metodo, gli altri devono avere la parola chiave [default].

Il metodo [Predicate<T>.test] riceve un parametro di tipo T e restituisce un valore booleano. Questa interfaccia viene generalmente utilizzata per filtrare le collezioni. Per illustrarne l’utilizzo, 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};

  // dati
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // costruttori
  public Personne() {

  }

  public Personne(String nom, Sexe sexe, int age, double poids) {
    this.nom = nom;
    this.sexe=sexe;
    this.age = age;
    this.poids = poids;
  }

  // getter e setter
...

  // 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 relativa alla persona;

La classe [Personnes] 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 ottenere questo elenco;
  • righe 17-23: un metodo statico che consente di ottenere la stringa jSON da 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) {
    // predicato implementato da una classe anonima
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
// predicato implementato da un lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // elenco di persone
    List<Personne> personnes = Personnes.get();
    // visualizzazioni
    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] filtra l'elenco [personnes]
    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<Persona> tramite una classe anonima. Si tratta di un filtro basato sul peso della persona;
  • riga 20: implementazione dell'interfaccia Predicate<Persona> tramite una funzione lambda. Si tratta di un filtro sull'età della persona. In base a quanto detto, avrebbe potuto essere scritta anche nel modo seguente:

        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 si costruisce un tipo [Predicate<Personne>]. Il metodo implementato ha quindi come firma [boolean test(Personne param)]. Pertanto, il tipo implicito di p, riga 20, è il tipo [Personne];

  • riga 22: si recupera un elenco predefinito di persone;
  • riga 24: le si filtra in base all’età;
  • riga 25: le si filtra in base al peso. In entrambi i casi, si visualizza la stringa jSON dell’elenco così filtrato;
  • righe 28-37: un metodo statico che
    • accetta come parametri: un elenco di persone da filtrare e il filtro. Quest'ultimo è un'istanza dell'interfaccia [Predicate<Personne>]. Per convenzione, qui e altrove nel documento, con «istanza di un'interfaccia I» si intende un'istanza di una classe C che implementa I;
    • restituisce come risultato l’elenco così filtrato;
  • riga 32: si utilizza 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] fornisce 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, a partire da un tipo Collection<T>, un nuovo tipo Collection<R>. Per illustrare questa interfaccia, utilizzeremo 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 {
    // implementazione con classe anonima
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implementazione con lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // elenco di persone
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // visualizzazioni
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // trasformazione da List<Persona> a 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: si implementa l’interfaccia [Function<Personne, String>] con una classe anonima che effettuerà la trasformazione da Personne a String;
  • riga 22: si implementa l'interfaccia [Function<Personne, Integer>] con una funzione lambda che effettuerà la trasformazione da Personne a Integer;
  • righe 33-39: un metodo statico che
    • accetta due parametri: il primo, di tipo List<Personne>, è un elenco di persone da trasformare; il secondo, di tipo Function<Persona, T>, è una funzione che, a partire da ciascuna persona dell’elenco, crea un oggetto di tipo T;
    • restituisce come risultato un tipo List<T> in cui ogni elemento deriva dalla trasformazione Persona -> T;
  • righe 35-37: si applica la trasformazione Persona -> T. Se il secondo parametro del metodo è l’oggetto [mapToName], verrà eseguita una trasformazione Persona -> String. Se si tratta dell’oggetto [mapToAge], verrà eseguita una trasformazione Persona -> Integer;

Il risultato ottenuto è 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 utilizza (consuma) il proprio parametro e non restituisce alcun risultato. Per illustrarlo, 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) {
        // elenco di persone
        List<Personne> personnes = Personnes.get();
        // implementazione anonima
        Consumer<Personne> consumerAge = new Consumer<Personne>() {
            @Override
            public void accept(Personne personne) {
                System.out.printf(" age de %s = %s%n", personne.getNom(), personne.getAge());
            }
        };
        // implementazione lambda
        Consumer<Personne> consumerPoids = p -> System.out.printf(" poids de %s = %s%n", p.getNom(), p.getPoids());        
        // visualizzazioni
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • righe 14-19: l'interfaccia [Consumer<Personne>] è implementata da una classe anonima il cui metodo [accept] visualizza il nome e l'età della persona;
  • riga 21: l'interfaccia [Consumer<Personne>] è 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 utilizzato dall'implementazione [consumerAge];
  • righe 27-29: l'elenco delle persone viene utilizzato dall'implementazione [consumerPoids];

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). Accetta il tipo T con l'informazione aggiuntiva U u. Ne illustreremo l'utilizzo 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) {
    // elenco di persone
    List<Personne> personnes = Personnes.get();
    // implementazione anonima
    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());
      }
    };
    // implementazione lambda
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // visualizzazioni
    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> con una classe anonima. Il metodo [apply] utilizza il suo secondo parametro per aggiornare l’età della persona passata come primo parametro. Successivamente visualizza il risultato;
  • righe 22-25: implementazione dell'interfaccia BiConsumer<T,U> con una funzione lambda. Il metodo implicito [apply] utilizza il suo secondo parametro per aggiornare il peso della persona passata come primo parametro. Successivamente visualizza il risultato;
  • righe 27-29: l'elenco delle persone viene elaborato con l'implementazione [biconsumerAge];
  • righe 31-33: l'elenco delle persone viene elaborato con l'implementazione [biconsumerPoids];

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 alcun risultato, il metodo [BiFunction.apply] restituisce un risultato. Ne illustreremo l'utilizzo con il seguente codice:


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) {
        // elenco di persone
        List<Personne> personnes = Personnes.get();
        // implementazione anonima
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // implementazione lambda
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // visualizzazioni
        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<Personne, Integer, Integer> è implementata con una classe anonima. Il metodo [apply] di quest'ultima restituisce l'età della persona passata come primo parametro, aumentata del valore del secondo parametro;
  • righe 21-23: l'interfaccia BiFunction<Persona, Integer, Double> è implementata con una funzione lambda. Il metodo [apply] restituisce il peso della persona passata come primo parametro, aumentato del valore del secondo parametro;
  • righe 25-27: si applica l'implementazione [biFunctionAge] alle persone;
  • righe 29-31: si applica l'implementazione [biFunctionPoids] 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 essere sottoposti a trasformazioni successive implementate da funzioni lambda. Quando possibile, e in presenza di più processori, tali 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 fornire un oggetto di tipo T.

Illustreremo questa interfaccia funzionale con il codice seguente:


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) {
        // implementazione anonima
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // elenco di persone
            List<Personne> personnes = Personnes.get();

            // implementazione dell'interfaccia
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // elaborazione
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // visualizzazione persona
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • righe 13-28: implementazione di un tipo Supplier<Personne>;
  • righe 31-33: il metodo statico [affiche] accetta un parametro di tipo Supplier<Personne>;
  • righe 25-27: esecuzione dell'istanza Supplier<Personne>;

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