L’histoire de Java a été marquée par deux évolutions importantes la première est l’introduction depuis JDK5 du concept de généricité et le second est celui des lambda expressions dont nous allons parler durant ce post.
Depuis JDK8 les lambda expressions a sensiblement changé la façon dont on peut écrire du Java et ceux de deux façons.
- La première est par l’ajout d’éléments syntaxique qui augmente l’expressivité du langage.
- la seconde est d’avoir ajouté de nouvelle possibilité au langage (tel que la facilité du parallélisme, et l’utilisation de la nouvelle API Stream).
Introduction aux lambda expressions:
La clé pour bien comprendre les lambda expressions est de comprendre en premier la syntaxe des lambdas et ensuite de comprendre les Interface fonctionnelles.
Une petite définition s’impose:
- une lambda expression est essentiellement une méthode anonyme. cette méthode n’est pas faite pour être exécuté par elle-même mais pour implémenter une méthode définie dans une Interface fonctionnelle.
- une Interface fonctionnelle est une interface qui contient une seule (pas plus) méthode abstraite qui représente une seule action. l’interface fonctionnelle représente donc le type de la lambda.
Les fondamentaux:
Une nouvelle syntaxe est introduite: l’opérateur -> qui divise l’expression en deux partie, la partie de gauche est celle des paramètres requis par la lambda expression, la partie de droite et celle du corps de la lambda.
Le corps peut être une instruction simple ou un bloc de code:
()-> 12 // une expression lambda avec 0 arguments et une constante en retour
(a) -> {
int result = 1;
for (int i = 1; i<=a; i++)
result = i*result;
return result;
};// une expression lambda avec comme argument la valeur a et un bloc d'instruction
Functional Interfaces:
Comme indiqué précédemment une interface fonctionnelle est une interface qui ne contient qu’une seule méthode. Après Jdk8 une interface est abstraite si elle ne définit pas une implémentation par default, plus besoin d’utiliser le mot clé abstract.
Exemple d’une interface abstraite:
interface MyInterface{
int getValue();
}
La lambda fera une implémentation de la méthode de son interface fonctionnel.
Exemples:
Exemple 1: lamba sans arguments
public class Lambda1 {
public static void main(String[] args) {
MyInterface myInterface;//Declaration de l'interface fonctionnel
myInterface = () -> 12; //affectation de la lambda a l'interface qui contiendra l'implemetation de la methode getValue
System.out.println(myInterface.getValue());
myInterface = () -> (int)Math.round(Math.random()*100);
System.out.println(myInterface.getValue());
}
//Definition d'une interface fonctionnel
interface MyInterface{
int getValue();
}
L’expression lambda doit être compatible avec la valeur retourné par la méthode abstraite définie dans l’interface fonctionnel.
Exemple 2: lambda avec arguments
public class Lambda2 {
public static void main(String[] {
MyArgumentedInterface isPaire= (n) -> (n%2)==0;//declaration et implementation de la method abstraite testing
if(isPaire.testing(12)) System.out.println("Paire");
else System.out.println("Impaire");
MyArgumentedInterface isNegative = n -> n<0;//une autre lambda expression qui redefinie la methode abstraite testing
System.out.println("est il negatif : "+isNegative.testing(-21));
}
//Definition d'une interface fonctionnel qui prend en argument un int et qui retourne un boolean
interface MyArgumentedInterface{
boolean testing(int n);
}
Dans cet exemple nous avons créé une interface fonctionnelle qui déclare une méthode abstraite testing, cette méthode prend un argument int et retourne un boolean.
Cette interface nous permettra de définir toute sorte de lambda expression qui effectue un teste quelconque sur un entier.
Exemple 3: quelques exemples
public class Lambda3 {
public static void main(String[] {
MyTwoArgument myTwoArgument = (a,b)-> a+b;
System.out.println(myTwoArgument.operation(2,2));
NumericFonction facto = (a) -> {
int result = 1;
for (int i = 1; i<=a; i++)
result = i*result;
return result;
};
System.out.println(facto.calcul(5));
StringOp stringOp = (s) -> {
String result ="";
for (int i=s.length()-1;i>=0;i--){
result += s.charAt(i);
}
return result;
};
System.out.println(stringOp.opString("Amin KAMAL"));
}
interface MyTwoArgument{
int operation(int a,int b);
}
interface NumericFonction{
int calcul(int a);
}
interface StringOp{
String opString(String s);
}
Exemple 4 : Interface generique
Nous allons donc dans cet exemple voir comment créer une interface commune pour tous nos besoins.
public class Lambda4 {
public static void main(String[] {
//Passer des lambda expression en argument d'une fonction
//le principe est de definir l'interface fonctionnel et de creer une methode qui prend en argument une fonction
//de type interface fonctionnel et l'argument a transformer puis de rendre la fonction qui sera redefini dans l'appel
//de la fonction.
String inStr = "Voici la chaine en entree";
String outStr;
System.out.println("The input string : "+inStr);
outStr = stringOpe((str) -> str.toUpperCase(), inStr);
System.out.println("The output string :"+outStr);
//Effacer les espaces
outStr = stringOpe(str -> {
String res = "";
for(int i=0;i<str.length()-1;i++)
if (str.charAt(i) != ' ')
res += str.charAt(i);
return res;
},outStr);
System.out.println(outStr);
}
static String stringOpe(MyGenericInterface<String> strlambda,String s){
return strlambda.func(s);
}
}
//Generic Functional Interface
interface MyGenericInterface<T>{
T func(T t);
}
Nous avons aussi pour montrer le passage d’une lambda expression en argument d’une méthode créé la fonction stringOpe qui prend donc en argument la définition de la lambda expression et qui l’utilise en faisant appel à la fonction func qui est définie dans l’interface fonctionnelle. Donc implémentation de func dans les arguments de la méthode.
Exemple 5 : lambda et exceptions
public class MainLambdaException {
public static void main(String[] args) throws IllegalExceptionDivisionNull {
IllegalDivision<Double> division = (n, t) -> {
if(t==0){
throw new IllegalExceptionDivisionNull();
}
return n/t;
};
System.out.println("Division de "+12+"/"+2+"= "+division.func(12.0,2.0));
System.out.println("Division de "+12+"/"+2+"= "+division.func(12.0,0.0));
}
}
//Definition d'une interface abstraite generique
interface IllegalDivision<T>{
T func(T t,T r) throws IllegalExceptionDivisionNull;
}
//Definition d'une class pour l'exception de definition par 0
class IllegalExceptionDivisionNull extends Exception{
IllegalExceptionDivisionNull(){
super("Division par 0 impossible");
}
}
Exemple 6 : Methode References
la syntaxe : ClassName::methodName les :: est introduit depuis JDK8
public class MethodRefExemple {
//Methode static qui prend en argument une lambda expression qui redefinie l'interface fonctionnel ainsi qu'une chaine de caractere
static String stringOperation(MyStringFunctionalInterface<String> stringOp, String str){
return stringOp.stringop(str);//retourne le resultat de la redefinition de la lambda expression
}
public static void main(String[] args) {
String inString = "Bonjour et bienvenue sur le blog du developpeur";
String outString;
//ici on lance la methode static precedement defini avec comme argument la reference a une methode appartenant a la class
outString = stringOperation(MyStringOperation::reverseString, inString);
System.out.println("Resultat : "+outString);
}
}
//interface Fonctionnel generic
interface MyStringFunctionalInterface <T>{
T stringop(T t);
}
//Class utiliser pour l'appel a la methode de reference
class MyStringOperation{
static String reverseString(String str){
String result = "";
for (int i= str.length()-1;i>=0;i--)
result += str.charAt(i);
return result;
}
}
Dans cette exemple une méthode static de référence reverseString déclarer dans une class et qui est passé comme argument de la méthode static stringOperation. Vu que la méthode reverseString est compatible avec l’interface fonctionnel les type de retour fonctionne. ainsi l’expression ‘MyStringOperation::reverseString’ est évalué ce qui donne l’implémentation de la méthode strinop de l’interface fonctionnel.
Il est aussi possible de faire référence a la méthode d’une instance de class:
//Nous gardons exactement le meme exemple sauf qu'a la ligne 14 nous changeons l'appel a la methode qui redefini la methode de l'interface fonctionnel
...
MyStringOperation myOp = new MyStringOperation();
outString = stringOperation(myOp::reverseString, inString);
...
//il faut vu qu'on appel une instance de l'objet declarer la methode comme non static
...
class MyStringOperation{
String reverseString(String str){
...
}
}
Interfaces Fonctionnel prédéfinies:
Nous savons à présent comment fonctionnent les lambda expressions, nous avons créé nos propre interfaces fonctionnel qu’elle soit generic ou pas. un point important lors de l’utilisation des lambda est de savoir que la plupart des interfaces fonctionnel dont nous avons besoin sont fourni dans le package java.util.function, dans la plupart des cas avant de penser à réécrire une interface fonctionnel et pour appliquer le principe de « Don’t repeat yourself » il faut d’abord voir si notre besoin n’est pas déjà fourni par java.util.function (dans la javadoc vous pouvez voir toute la liste des interfaces fonctionnelles ainsi que leurs définitions).
Pour mieux comprendre ce point nous allons présenter quelque unes des interfaces fonctionnel les plus couramment utilisé (Predicate, Consumer, Function et Supplier).
| Interface Fonctionnel | Description | Cas frequents d'utilisation | methode abstraite |
|---|---|---|---|
| Predicate <T> | Teste une condition et retourne un boolean | utilisé souvent en tant que filtre dans les Stream par exemple pour supprimer les elements qui ne verifie pas certaines conditions | boolean test(T t) |
| Consumer <T> | Opération prenant un argument mais ne retourne rien | pour parcourir des collections ou un stream | void accept(T t) |
| Function<T,R> | Fonction prenant un argument et retournant un resultat | souvant utilisé dans les map pour prendre un argument et retourné un resultat | R apply(T t) |
| Supplier<T> | Opération retournant une valeur a l’appelant | dans la method generate() d’un stream | T get() |
l’Interface Predicate:
Comme vu dans le tableau ci-dessus cette interface sert à faire des tests en prenant un paramètre à tester et en retournant un boolean
un exemple d’utilisation dans un Stream:
public class MainPredicate {
public static void main(String[] args) {
Stream.of("teste","predicate") // creation d'un stream avec deux chaines de carateres
.filter(str -> str.contains("ca"))//filtrer le stream en utilisant une lambda expression de type Predicate prenant une chaine et retournant le resultat de contains
.forEach(System.out::println);
}
}
>> predicate
le point important ici est l’utilisation de filter dont voici la définition :
filter prend en argument un Predicate donc une interface fonctionnel que nous allons pouvoir redéfinir avec les lambda expression et retourne un nouveau stream selon le boolean retourné par tes(T t) qui est la méthode abstraite de Predicate et qui est redéfinie par la lambda expression, dans notre cas str.contains(« ca »)
Stream<T> filter(Predicate<? super T> predicate);
On peut donc écrire le code précèdent comme suit:
public static void main(String[] args) {
Predicate <String> containca = str -> str.contains("ca");
Stream.of("teste","predicate")
.filter(containca)
.forEach(System.out::println);
}
Il est aussi possible de combiner des Predicate en utilisant des méthode logique qui prennent un Predicate et retourne un Predicate:

ce qui donne le code suivant:
public class MainPredicate {
public static void main(String[] args) {
Predicate <String> containca = str -> str.contains("ca");
Predicate <String> isbeginwithp = str -> str.startsWith("pr");
Stream.of("teste","predicate")
.filter(containca.and(isbeginwithp))
.forEach(System.out::println);
}
}
exemple d’application sur les collection:
List<String> myList = new ArrayList<>();
myList.add("Bonjour");
myList.add("tous le monde");
myList.removeIf(str -> !str.startsWith("B"));
//equivalent a
myList.removeIf(((Predicate<String>) str -> str.startsWith("B")).negate());
myList.forEach(System.out::println);
avec la methode removeIf qui prend en argument un Predicate ici definie par la lamba

l’Interface Consumer:
Comme indique dans le tableau c’est une interface fonctionnel qui définit une méthode abstraite qui prend un argument et retourne void.
quelques exemples de consumers :
le forEach par exemple dans l’exemple précèdent prend en argument un consumer qui prend une méthode de référence l’exemple peut être écrit comme suit:
public class MainConsumersExemple {
public static void main(String[] args) {
Stream.of("Salut","les","consomateurs")
.forEach((Consumer<String>) str-> System.out.println(str.toUpperCase())) ;
}
}
l’interface Function:
Prend un type T et retourne un Type R la lambda expression dans notre exemple fera un appel à la méthode de référence parseint:
Dans le premier map il prend un string depuis le stream et retourne un int et sur le second la lambda prend un int et retourn un int
Arrays.stream("5,10,-20".split(","))
.map(Integer::parseInt)
.map(integer -> (integer <0)? -integer:integer)
.forEach(System.out::println);
public class CombineFunctions {
public static void main(String []args) {
Function<String, Integer> parseInt = Integer::parseInt;
Function<Integer, Integer> absInt = Math::abs;
Function<String, Integer> parseAndAbsInt = parseInt.andThen(absInt);
Arrays.stream("4, -9, 16".split(", "))
.map(parseAndAbsInt)
.forEach(System.out::println);
}
}
l’interface Supplier:
Dans le cas de l’utilisation d’une méthode qui ne prend aucun paramètre mais retourne une type quelconque nous pouvons utiliser l’interface fonctionnelle supplier.
Prenons l’exemple suivant qui créer un stream en utilisant la méthode générale avec en argument un supplier ici un appel à la méthode de reference nextBoolean
Random random = new Random();
Stream.generate(random::nextBoolean)
.limit(12)
.forEach(System.out::println);
voila la definition de la methode generate:
static<T> Stream<T> generate(Supplier<T> s)
Voici un exemple d’utilisation d’un suppiler qui genere une valeur sans aucun argument.
Supplier<String> currentDateTime = () -> LocalDateTime.now().toString();
System.out.println(currentDateTime.get());