Skip to content

4. Les fonctions lambdas de Java 8

4.1. Exemple-01 - Interfaces fonctionnelles et lambdas

  

Considérons le code suivant :


package dvp.java8.lambdas;

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

    // classes anonymes
    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);
    };

    // appli
    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);
}
  • lignes 41-44 : définissent une interface fonctionnelle I1. Une interface fonctionnelle est une interface n'ayant qu'une méthode. Elle n'est pas liée à la présence de l'annotation [@FunctionalInterface] de la ligne 41 qui est facultative ;
  • lignes 46-49 : une deuxième interface fonctionnelle I2 ;
  • lignes 6-19 : les interfaces I1 et I2 sont implémentées avec des classes anonymes, la solution la plus courante avant l'introduction des fonctions lambdas ;
  • lignes 21-29 les interfaces I1 et I2 sont implémentées avec des fonctions lambdas ;
  • lignes 7-12 : implémentation de l'interface I1 avec une classe anonyme. La syntaxe pour implémenter une interface I avec une classe anonyme est la suivante :
I i=new I(){
    @Override
    public T1 m1(...){
...
    }
    public T2 m2(...){
...
    }
}

où m1, m2, ... sont les méthodes définies par l'interface I.

  • lignes 14-19 : implémentation de l'interface I2 avec une classe anonyme ;
  • ligne 22 : implémentation de l'interface I1 avec une fonction lambda. On profite ici du fait que l'interface fonctionnelle n'a qu'une méthode. La fonction lambda implémente alors cette unique méthode M. Sa syntaxe est la suivante :
(T1 param1, T2 param2, ...) -> {implémentation de la méthode M ;}

Les types T1, T2, Tn peuvent être omis si le compilateur peut les déduire du contexte (inférence de type).

  • ligne 22 : implémentation de la méthode [I1.doSomething] de signature :

void doSomething();

[doSomething] est une méthode qui n'a aucun paramètre et qui ne rend pas de résultat. Son implémentation lambda peut être écrite comme dans la ligne 22 ou bien commes aux lignes 24-26, ç-à-d qu'on peut mettre des accolades autour du code de la fonction lambda. Si ce code ne comporte qu'une instruction comme ici, ces accolades peuvent être omises ;

  • ligne 23 : implémentation de la méthode [I1.getSomething] de signature :

String getSomething(double value);

[getSomething] admet un paramètre de type [double] et rend un résultat de type [String]. Son implémentation lambda peut être celle de la ligne 23 ou celle des lignes 27-29. Dans l'implémentation de la ligne 23 :

  • le type du paramètre [value] est omis. Ce sera alors le type [double] trouvé dans la signature de [getSomething] qui sera utilisé ;
  • le code du lambda n'est pas entouré de parenthèses. Le résultat du lambda est alors la valeur de l'unique expression de ce code, ici : String.format("ib2.lambda(%s)", value) ;

Dans l'implémentation des lignes 27-29 :

  • on déclare explicitement le type du paramètre [value] ;
  • on utilise un [return] pour rendre le résultat du lambda. Dans ce cas, il faut mettre des accolades ;
  • lignes 32-37 : on appelle les diverses fonctions anonymes et lambdas ;

Le résultat obtenu est le suivant :

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. Exemple-02 - l'interface fonctionnelle Predicate<T>

  

La plupart du temps, nous avons affaire à des interfaces fonctionnelles de bibliothèques plutôt qu'à des interfaces fonctionnelles que nous définissons nous-mêmes. Ici, nous nous intéressons à l'interface fonctionnelle [Predicate] définie dans le package [java.util.function] qui rassemble la plupart des interfaces fonctionnelles de Java 8. Celle-ci est définie de la façon suivante :

Image

Nous avons dit qu'une interface fonctionnelle n'avait qu'une méthode. Or ici, il y en a quatre. Une autre innovation apportée par Java 8 a été d'introduire la notion de méthode par défaut d'une interface, caractérisée par le mot clé [default]. Nous avons ici trois telles méthodes. Ces méthodes ont la particularité d'avoir une implémentation par défaut. Il n'y a alors pas d'obligation pour une classe implémentant une interface avec des méthodes par défaut d'implémenter ces dernières. Ainsi, une classe voulant implémenter l'interface [Predicate] n'a qu'une méthode à implémenter obligatoirement : la méthode [test]. L'interface [Predicate] est donc bien une interface fonctionnelle. On dira donc qu'une interface fonctionnelle est une interface dont l'implémentation ne comporte qu'une méthode obligatoire. Si elle a plus d'une méthode, les autres doivent avoir le mot clé [default].

La méthode [Predicate<T>.test] reçoit un paramètre de type T et rend un booléen. Cette interface est en général utilisée pour filtrer des collections. Pour illustrer son utilisation, nous utiliserons les données suivantes :


package dvp.data;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Personne {

  public enum Sexe {HOMME,FEMME};

  // données
  private String nom;
  private int age;
  private double poids;
  private Sexe sexe;

  // constructeurs
  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 et setters
...

  // toString
  @Override
  public String toString(){
    try {
      return new ObjectMapper().writeValueAsString(this);
    } catch (JsonProcessingException e) {
      return e.getMessage();
    }
  }
}
  • lignes 32-38 : la méthode [toString] rend la chaîne jSON de la personne ;

La classe [Personnes] définit une liste de 3 personnes :


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();
    }
  }
}
  • lignes 10-11 : la liste de 3 personnes ;
  • lignes 13-15 : une méthode statique pour avoir cette liste ;
  • lignes 17- 23 : une méthode statique permettant d'avoir la chaîne jSON d'une liste de personnes passée en paramètre ;

Ces données seront exploitées par le code suivant :


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 implémenté par classe anonyme
    Predicate<Personne> filterPoids=new Predicate<Personne>() {
      @Override
      public boolean test(Personne personne) {
        return personne.getPoids()<50;
      }
    };
 // predicate implémenté par un lambda
    Predicate<Personne> filterAge = p -> p.getAge() < 28;
    // liste de personnes
    List<Personne> personnes = Personnes.get();
    // affichages
    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] filtre la liste [personnes]
    List<Personne> personnesFiltrées = new ArrayList<>();
    for (Personne p : personnes) {
      if (filter.test(p)) {
        personnesFiltrées.add(p);
      }
    }
    return personnesFiltrées;  }
}
  • lignes 13-18 : implémentation de l'interface Predicate<Personne> par une classe anonyme. il s'agit d'un filtre sur le poids de la personne ;
  • ligne 20 : implémentation de l'interface Predicate<Personne> par une fonction lambda. Il s'agit d'un filtre sur l'âge de la personne. D'après ce qui a été dit, elle aurait pu être écrite également de la façon suivante :

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

mais la version de la ligne 20 est plus concise. Le type du paramètre p est déduit du contexte. On construit ici un type [Predicate<Personne>]. La méthode implémentée a alors pour signature [boolean test(Personne param)]. Donc le type implicite de p, ligne 20, est le type [Personne] ;

  • ligne 22 : on récupère une liste de personnes prédéfinie ;
  • ligne 24 : on les filtre selon leur âge ;
  • ligne 25 : on les filtre selon leur poids. Dans les deux cas, on affiche la chaîne jSON de la liste ainsi filtrée ;
  • lignes 28-37 : une méthode statique qui
    • admet pour paramètres : une liste de personnes à filter et le filtre. Celui-ci est une instance de l'interface [Predicate<Personne>]. Par abus de langage, on désigne, ici et ailleurs dans le document, par instance d'une interface I, une instance d'une classe C implémentant I ;
    • rend comme résultat, la liste ainsi filtrée ;
  • ligne 32 : on utilise la méthode [test] de l'interface [Predicate]. Selon le filtre passé à la méthode, la méthode [test] sera :

return personne.getPoids()<50;

ou bien


return p.getAge() < 28

L'exécution de la classe [Exemple02] donne le résultat suivant :

[{"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. Exemple-03 - l'interface fonctionnelle Function<T,R>

  

L'interface fonctionnelle Function<T,R> est définie de la façon suivante :

Image

L'unique méthode de l'interface a la signature R apply(T t). On l'utilise en général pour créer à partir d'un type Collection<T> un nouveau type Collection<R>. Pour illustrer cette interface, nous utiliserons le code suivant :


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 {
    // implémentation avec classe anonyme
    Function<Personne, String> mapToName = new Function<Personne, String>() {
      @Override
      public String apply(Personne personne) {
        return personne.getNom();
      }
    };
    // implémentation avec lambda
    Function<Personne, Integer> mapToAge = p -> p.getAge();
    // liste de personnes
    List<Personne> personnes = Personnes.get();
    // jSON
    ObjectMapper jsonMapper = new ObjectMapper();
    // affichages
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToName)));
    System.out.println(jsonMapper.writeValueAsString(mapPersonnes(personnes, mapToAge)));
  }

  // transformation List<Personne> --> 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;
  }
}
  • lignes 15-20 : on implémente l'interface [Function<Personne, String>] avec une classe anonyme qui fera la transformation Personne --> String ;
  • ligne 22 : on implémente l'interface [Function<Personne, Integer>] avec une fonction lambda qui fera la transformation Personne --> Integer ;
  • lignes 33-39 : une méthode statique qui
    • admet deux paramètres : le premier de type List<Personne> est une liste de personnes à transformer. Le second de type Function<Personne, T> est une fonction qui à partir de chaque personne de la liste crée un objet de type T ;
    • rend pour résultat un type List<T> où chaque élément provient de la transformation Personne -> T ;
  • lignes 35-37 : on applique la transformation Personne -> T. Si le second paramètre de la méthode est l'objet [mapToName], c'est une transformation Personne -> String qui sera faite. S'il s'agit de l'objet [mapToAge], c'est une transformation Personne -> Integer qui sera faite ;

Le résultat obtenu est le suivant :

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

4.4. Exemple-04 - l'interface fonctionnelle Consumer<T>

  

L'interface fonctionnelle Consumer<T> est définie de la façon suivante :

Image

L'unique méthode de l'interface a la signature : void accept(T t). Cette méthode exploite (consomme) son paramètre et ne rend aucun résultat. Pour l'illustrer, nous utiliserons le code suivant :


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) {
        // liste de personnes
        List<Personne> personnes = Personnes.get();
        // implémentation anonyme
        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());        
        // affichages
        for (Personne p : personnes) {
            consumerAge.accept(p);
        }
        System.out.println("--------");
        for (Personne p : personnes) {
            consumerPoids.accept(p);
        }
    }
}
  • lignes 14-19 : l'interface [Consumer<Personne>] est implémentée par une classe anonyme dont la méthode [accept] affiche les nom et age de la personne ;
  • ligne 21 : l'interface [Consumer<Personne>] est implémentée par une fonction lambda dont la méthode [accept] implicite affiche les nom et poids de la personne ;
  • lignes 23-25 : la liste des personnes est consommée par l'implémentation [consumerAge] ;
  • lignes 27-29 : la liste des personnes est consommée par l'implémentation [consumerPoids] ;

Les résultats sont les suivants :

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. Exemple-05 - l'interface fonctionnelle BiConsumer<T,U>

  

L'interface fonctionnelle BiConsumer<T,U> est définie de la façon suivante :

Image

L'unique méthode de l'interface a la signature : void accept(T t, U u). Elle consomme le type T avec l'information supplémentaire U u. Nous illustrerons son usage avec le code suivant :


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) {
    // liste de personnes
    List<Personne> personnes = Personnes.get();
    // implémentation anonyme
    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());
      }
    };
    // implémentation lambda
    BiConsumer<Personne, Integer> biconsumerPoids = (p, i) -> {
      p.setPoids(p.getPoids() + i);
      System.out.printf("poids de %s = %s%n", p.getNom(), p.getPoids());
    };
    // affichages
    for (Personne p : personnes) {
      biconsumerAge.accept(p, 100);
    }
    System.out.println("--------");
    for (Personne p : personnes) {
      biconsumerPoids.accept(p, 200);
    }
  }
}
  • lignes 14-20 : implémentation de l'interface BiConsumer<T,U> avec une classe anonyme. La méthode [apply] utilise son deuxième paramètre pour mettre à jour l'âge de la personne passée en premier paramètre. Elle affiche ensuite le résultat ;
  • lignes 22-25 : implémentation de l'interface BiConsumer<T,U> avec une fonction lambda. La méthode [apply] implicite utilise son deuxième paramètre pour mettre à jour le poids de la personne passée en premier paramètre. Elle affiche ensuite le résultat ;
  • lignes 27-29 : la liste des personnes est consommée avec l'implémentation [biconsumerAge] ;
  • lignes 31-33 : la liste des personnes est consommée avec l'implémentation [biconsumerPoids] ;

Les résultats obtenus sont les suivants :

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. Exemple-06 - l'interface fonctionnelle BiFunction<T,U,R>

  

L'interface fonctionnelle BiFunction<T,U,R> est définie de la façon suivante :

Image

L'unique méthode de l'interface a la signature : R apply(T t, U u). Cette méthode est proche de la méthode [BiConsumer.apply] mais alors que cette dernière ne rend pas de résultat, la méthode [BiFunction.apply] rend un résultat. Nous illustrerons son utilisation avec le code suivant :


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) {
        // liste de personnes
        List<Personne> personnes = Personnes.get();
        // implémentation anonyme
        BiFunction<Personne, Integer, Integer> biFunctionAge = new BiFunction<Personne, Integer, Integer>() {
            @Override
            public Integer apply(Personne personne, Integer integer) {
                return personne.getAge() + integer;
            }
        };
        // implémentation lambda
        BiFunction<Personne, Integer, Double> biFunctionPoids = (p, i) -> {
            return p.getPoids() + i;
        };
        // affichages
        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));
        }
    }
}
  • lignes 14-19 : l'interface BiFunction<Personne, Integer, Integer> est implémentée avec une classe anonyme. La méthode [apply] de celle-ci rend l'âge de la personne passée en 1er paramètre augmenté de la valeur du second paramètre ;
  • lignes 21-23 : l'interface BiFunction<Personne, Integer, Double> est implémentée avec une fonction lambda. La méthode [apply] rend le poids de la personne passée en 1er paramètre augmenté de la valeur du second paramètre ;
  • lignes 25-27 : on applique l'implémentation [biFunctionAge] aux personnes ;
  • lignes 29-31 : on applique l'implémentation [biFunctionPoids] aux personnes ;

Les résultats obtenus sont les suivants :

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

Outre les fonctions lambdas, Java 8 a introduit le type Stream<T> qui modélise un flux d'éléments de type T. Ces éléments peuvent subir des transformations successives implémentées par des fonctions lambda. Lorsque c'est possible, et qu'il y a plusieurs processeurs, ces transformations peuvent parfois se faire en parallèle.

4.7. Exemple-07 - l'interface fonctionnelle Supplier<T>

  

L'interface fonctionnelle Supplier<T> est définie de la façon suivante :

Image

L'unique méthode de l'interface a la signature : T get() dont le rôle est de fournir un objet de type T.

Nous illustrerons cette interface fonctionnelle avec le code suivant :


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) {
        // implémentation anonyme
        Supplier<Personne> supplier = new Supplier<Personne>() {
            // liste de personnes
            List<Personne> personnes = Personnes.get();

            // implémentation interface
            @Override
            public Personne get() {
                int i = new Random().nextInt(personnes.size());
                return personnes.get(i);
            }
        };
        // exploitation
        for (int i = 0; i < 5; i++) {
            affiche(supplier);
        }
    }

    // affichage Personne
    public static void affiche(Supplier<Personne> supplier) {
        System.out.println(supplier.get());
    }
}
  • lignes 13-28 : implémentation d'un type Supplier<Personne> ;
  • lignes 31-33 : la méthode statique [affiche] attend un paramètre de type Supplier<Personne> ;
  • lignes 25-27 : exploitation de l'instance Supplier<Personne> ;

On obtient les résultats suivants :

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