Mis à jour le 30/04/2018
  • 40 heures
  • Difficile

Ce cours est visible gratuitement en ligne.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Classes anonymes, interfaces fonctionnelles, lambdas et références de méthode

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Dans ce chapitre nous allons voir des notions que vous utiliserez très souvent dans vos projets car elles permettent de créer des programmes en allégeant considérablement votre code.
Hormis les classes anonymes, les autres sujets traités dans ce chapitre sont des nouveautés de Java 8 et ne peuvent être utilisées que dans un environnement supérieur ou égal à celui-ci.

Les classes anonymes

Dans un chapitre précédent nous avons vu toute la puissance des interfaces. Mais vous avez dû vous rendre compte que que vous vous retrouvez alors avec un nombre de classes assez conséquent... Les classes anonymes permettent de faire exactement la même chose que ce que nous avons fait précédemment avec toute nos interfaces et classes abstraites mais sans avoir a créer de façon explicite une classe.
Il n'y a rien de compliqué dans cette façon de procéder, mais je me rappelle avoir été déconcerté lorsque je l'ai rencontrée pour la première fois.
Les classes anonymes sont le plus souvent utilisées pour la gestion d'actions ponctuelles, lorsque créer une classe pour un seul traitement est trop lourd. Rappelez-vous du code que nous avons utilisez pour tester le changement de comportement d'un personnage dans le chapitre sur les interfaces :

import com.sdz.comportement.*;

class Test{

  public static void main(String[] args) {
    Personnage pers = new Guerrier();
    pers.soigner();     
    pers.setSoin(new Operation());
    pers.soigner();     
  }
}

En suivant cet exemple, une classe anonyme permettant de modifier le comportement de soin de notre personnage ressemblera à ceci :

import com.sdz.comportement.*;

class Test{

  public static void main(String[] args) {
    Personnage pers = new Guerrier();
    pers.soigner();     
    pers.setSoin(new Operation());
    pers.soigner();
    
    //Utilisation d'une classe anonyme
    pers.setSoin(new Soin() {
		public void soigne() {
			System.out.println("Je soigne avec une classe anonyme ! ");
		}    	
    });
    
    pers.soigner();
  }
}

L'une des particularités de cette méthode, c'est que cette action n'est définie que pour cet objet. Nous devons seulement redéfinir la (ou les) méthode(s) de l'interface ou de la classe abstraite, que vous connaissez bien maintenant, dans un bloc d'instructions ; d'où les accolades après l'instanciation, comme le montre la figure suivante.

Découpage d'une classe anonyme

Pourquoi appelle-t-on cela une classe « anonyme » ?

C'est simple : procéder de cette manière revient à créer une classe fille sans être obligé de créer cette classe de façon explicite. L'héritage se produit automatiquement.Seulement, la classe créée n'a pas de nom, l'héritage s'effectue de façon implicite ! Nous bénéficions donc de tous les avantages de la classe mère en ne redéfinissant que la méthode qui nous intéresse.
Sachez aussi que les classes anonymes peuvent être utilisées pour implémenter des classes abstraites. Je vous conseille d'effectuer de nouveaux tests en utilisant notre exemple du pattern strategy ; mais cette fois, plutôt que de créer des classes, créez des classes anonymes.
Les classes anonymes sont soumises aux mêmes règles que les classes « normales » :

  • utilisation des méthodes non redéfinies de la classe mère ;

  • obligation de redéfinir toutes les méthodes d'une interface ;

  • obligation de redéfinir les méthodes abstraites d'une classe abstraite.

Cependant, ces classes possèdent des restrictions à cause de leur rôle et de leur raison d'être :

  • elles ne peuvent pas être déclarées  abstract  ;

  • elles ne peuvent pas non plus être déclarées  static  ;

  • elles ne peuvent pas définir de constructeur ;

  • elles sont automatiquement déclarées  final  : on ne peut dériver de cette classe, l'héritage est donc impossible !

Il y a moyen d'écrire encore moins de code avec les lambdas mais, avant, je dois vous parler du pillier des lambdas : les interfaces fonctionnelles

les interfaces fonctionnelles

Ce concept, introduit depuis Java 8, permet de définir une interface n'ayant qu'une et une seule méthode abstraite : c'est grâce à cette restriction qu'il sera possible d'utiliser les lambdas car, lors de l'exécution, Java pourra automatiquement déterminer quelle est la signature de la méthode que la lambda remplace et tout sera automatique.

Dans notre exemple précédent, notre interface Soin est une interface fonctionnelle car elle n 'a qu'une méthode à redéfinir (Single Abstract Method, ou interface SAM). Comme je vous le disais, ce type d'interface sera le pillier de l'utilisation des lambdas donc, pour s'assurer que le contrat est bien respecté, Java propose d'annoter l'interface avec  @FunctionalInterface  , comme ceci :

package com.sdz.comportement;
@FunctionalInterface
public interface Soin {
	public void soigne();
}

Dès lors, si vous ajoutez une méthode abstraite (même avec une signature différente), Java vous indiquera qu'il y a un problème.

Problème dans l'interface fonctionnelle
Problème dans l'interface fonctionnelle

Mais vous avez tout à fait le droit d'avoir une interface fonctionnelle avec des méthodes par défaut, comme le montre l'image ci-dessous :

Méthode par défaut dans une interface fonctionnelle
Méthode par défaut dans une interface fonctionnelle

Java 8 vient avec de nombreuses interfaces fonctionnelles prédéfinies dans le package java.util.function dont le détail est disponible à l'adresse suivante : https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

Nous en reparlerons à la fin de ce chapitre et nous aurons l'occasion d'en utiliser avec les lambdas, vous verrez, c'est magique !

Encore moins de code avec les lambdas !

Avant Java 8, il n’existait que deux types de références, des valeurs primitives (char c = 'C' ;) et des références vers des objets (String s = new String("Hello");). Dans d'autres langages par contre, il existe des références vers ce qu'on appelle des closures : des morceaux de codes anonymes. Jusqu'en Java 7, la seul façon d'avoir ce type de référence revenait a faire une classe anonyme comme vu précédemment. Depuis Java 8, les closures existent et s'appellent les lambdas. Le parallèles avec les classes/méthodes anonymes est volontaire car les deux se conçoivent de la même manière : elles peuvent prendre des arguments et retourne un résultat. Pour simplifier au maximum, une lambda est la redéfinition d'une méthode d'une interface fonctionnelle sans avoir à faire une classe anonyme, donc gain de ligne de code et de visibilité.

Ainsi, nous allons pouvoir rencontrer un nouvel opérateur : ->.
Gardez en mémoire qu'une lambda permet de redéfinir une méthode abstraite d'une interface fonctionnelle donc, par conséquent, elle doit répondre à la signature de la méthode concernée.Voici concrètement comment se construit une expression lambda :

() -> faire une action;
(param1, param2) -> faire une action;
(param1, param2, param3) -> {traitements ; retourne une valeur;} ;

Le plus simple pour appréhender ce nouvel opérateur est de vous le faire découvrir par le biais de plusieurs exemples pour vous expliquer cette syntaxe un peu spéciale...

() -> 1337 ;

 Ici, notre lambda ne prend aucun paramètre et retourne tout le temps 1337. Ca doit être la lambda la plus simple... Notez qu'ici, la lambda retourne la valeur 1337 alors que nous n'avons pas utilisé le mot clé return" : ceci est dû au fait que notre lambda ne fait qu'une et une seule tâche dans ce cas et uniquement dans ce cas, le mot clé return  ainsi que les accolades '{ }' sont facultatif. Par contre, il faut toujours terminer un lambda par un ';'. Cette lambda peut donc être utilisée pour remplacer une méthode fonctionnelle qui retourne un nombre (  int  ,  double  ,  float  ,  long  ) et ne prend pas de paramètre. Continuons avec d'autres exemples.

() -> { System.out.println("Bonjour lambda !") ; return 1337;} ;

 Ici, la lambda est presque identique à la précédente à l'exception près que là, nous affichons un message dans la console avant de retourner un entier. Ici nous avons donc plusieurs actions dans le corps de notre lambda, de ce fait, nous devons entourer tout le corps avec des accolades, sans oublier le ' ;' final ! Allez, un dernier exemple pour la route :

(a, b) -> {System.out.println("coucou"); return a + b;};

Ici, vous l'aurez compris, notre lambda sera à utiliser pour une interface fonctionnelle ayant une méthode abstraite attendant deux paramètres et retournant un type numérique.

Maintenant que vous êtes familier de la syntaxe des lambdas nous allons l'utiliser et, pour cela, nous allons avoir besoin d'une interface fonctionnelle : ça tombe bien, on en a justement quelques unes sous la main grâce au chapitre précédent mais, pour l'aspect pédagogique, nous allons créer une nouvelle interface, disons :

package com.sdz.comportement;
@FunctionalInterface
public interface Dialoguer {
	public void parler(String question);
}

Et pour bien vous montrer la différence de code, voici comment nous aurions dû redéfinir ceci avec les classes anonymes :

Dialoguer d = new Dialoguer() {
	public void parler(String question) {
		System.out.println("Tu as dis : " + question);	
	}
};
d.parler("Bonjour");

Et voilà le même code avec une lambda :

Dialoguer d = (s) -> System.out.println("Tu as dis : " + s);
d.parler("Bonjour");

Et voilà, votre première lambda. Vous venez de redéfinir la méthode abstraite de l'interface fonctionnelle Dialoguer  et vous l'avez utilisez. Notez bien que n'avez fait que redéfinir la méthode, pour l'utiliser vous devez l'invoquer.
Afin de décrire rapidement la signature d'une méthode abstraite présente dans une interface fonctionnelle, Java 8 a introduit le terme « Descripteur de fonction » qui permet de traduire la méthode en lambda. Ainsi, la description de fonction de notre interface d'exemple est : (String) -> void. Elle prend un paramètre de type String et ne retourne rien.  Pour terminer, deux petites recommandations :

  • Les lambdas doivent êtres concises, sinon le code ne sera pas plus lisible qu'avec une classe anonyme habituelle ;

  • elles doivent être simple à comprendre pour que le débogage reste trivial en cas de problème.

  • Ce sont deux choses à avoir à l'esprit sous peine de complexifier votre travail au lieu de le simplifier.

     

Le package java.util.function

Ce package qui fait également parti des nouveautés de Java 8 embarque une quarantaine d'interfaces fonctionnelles qui peuvent répondre à vos besoins sans que soyez obligé de faire votre propre interface notamment car celui-ci tire un grand avantage des génériques.

Parmi toutes les interfaces disponibles, voici cinq des interfaces utilisant des génériques et représentant un type de traitement :

  • java.util.function.Function<T,R>  : sa méthode fonctionnelle a la signature R  apply(T t)  . Elle permet donc de traité un paramètre T et de renvoyer un type R.

  • java.util.function.Predicate<T>  : sa méthode   boolean test(T t)  permet, comme vous vous en doutez, de faire un test sur le paramètre et de retourner un  boolean  en fonction du résultat.

  • java.util.function.Consumer<T>  : Cette interface fonctionnelle est un peu particulière car c'est la seule qui a pour vocation de modifier les données qu'elle reçoit. Sa méthode fonctionnelle   void accept(T t)  est faite pour appliquer des traitements au paramètre passer et ne retourne rien.

  • java.util.function.Supplier<T>  : Celle-ci permet de renvoyer un élément de type T sans prendre de paramètre via la méthode fonctionnelle  T get().

  • java.util.function.BinaryOperation  : S'utilise pour les opération de type reduce comme additionner deux  int  par exemple (on y reviendra lorsque nous parlerons des streams). Sa méthode   T apply(T t, T t2)  prend deux T en paramètre et renvoi un  T (T BynaryOperation(T,T).

Il existe des dérivées de ces interfaces qui spécifient un peut plus leur fonctionnement. Par exemple il existe une interface  IntFunction  dont la signature de la méthode  apply  est la suivante :  R apply(int value)  . L'interface renverra un type generique mais prendra un entier en paramètre. Il existe un bon nombre de ces interfaces et, en général, elles sont présentent pour chaque type mentionné ci-dessus. En voici une liste non exhaustive   IntFunction, IntSupplier, IntBinaryOperation, IntConsumer, IntToDoubleConsumer, IntToDoubleFunction, …
Comme je vous le disais, il y a de forte chance que vous trouviez ce dont vous avez besoin dans toutes ces interfaces fonctionnelles. Comme rien ne vaut la pratique, nous allons tout de suite utiliser ce package et, pour avoir des exemples simples, nous allons utiliser une classe très minimaliste :

java.util.function.Function<T,R>

Nous pouvons par exemple transformer une collection de Personne en collection de String représentant leurs noms ou en collection d'entier représentant leurs ages. Voici un code illustrant ceci :

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.*;

public class TestFunction {
	public static void main(String[] args) {
		
		List<Personne> lPersonne = Arrays.asList(
				new Personne(10, "toto"),
				new Personne(20, "titi"),
				new Personne(30, "tata"),
				new Personne(40, "tutu")
		);
		
		Function<Personne, String> f1 = (Personne p) -> p.getNom();
		Function<Personne, Integer> f2 = (Personne p) -> p.getAge() * 2;
		System.out.println(transformToListString(lPersonne, f1));
		System.out.println(transformToListInt(lPersonne, f2));
	}
	
	
	public static List<String> transformToListString(List<Personne> list, Function<Personne, String> func){
		List<String> ls = new ArrayList<>();
		for (Personne p : list) {
			ls.add(func.apply(p));
			//func.apply(p) retournera ici le nom de l'objet Personne
		}
		return ls;		
	}
	public static List<Integer> transformToListInt(List<Personne> list, Function<Personne, Integer> func){
		List<Integer> ls = new ArrayList<>();
		for (Personne p : list) {
			ls.add(func.apply(p));
			//func.apply(p) retournera ici l'âge multiplié par 2 de l'objet Personne
		}
		return ls;		
	}
}

Et ce code nous donne le résultat suivant :

[toto, titi, tata, tutu]
[20, 40, 60, 80]

C'est un code d'exemple, rien d'extravagant... Je vais en profiter pour vous montrer aussi comment surcharger une méthode par défaut de ces interfaces fonctionnelles. Par exemple, dans celle que nous venons d'utiliser, il y a la méthode  addThen  qui permet d'appliquer une fonction après le traitement. Par exemple, nous obtenons exactement le même résultat que précédemment avec ce code :

Function<Personne, String> f1 = (Personne p) -> p.getNom();
// On ne multiplie plus l'age par 2
Function<Personne, Integer> f2 = (Personne p) -> p.getAge();
// Nous définissons un traitement supplémentaire sur l'âge
Function<Integer, Integer> f3 = (Integer a) -> a * 2;
System.out.println(transformToListString(lPersonne, f1));
System.out.println(transformToListInt(lPersonne, f2.andThen(f3)));

java.util.function.Consumer<T>

Encore plus simple que la précédente interface car celle-ci ne retourne rien, elle se contente de "consommer" un objet, donc d'y appliquer un traitement, comme par exemple ajouter 13 ans à l'age d'un objet  Personne.

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class TestConsumer {
	public static void main(String[] args) {
		
		List<Personne> lPersonne = Arrays.asList(
				new Personne(10, "toto"),
				new Personne(20, "titi"),
				new Personne(30, "tata"),
				new Personne(40, "tutu")
		);
		
		System.out.println(lPersonne);
		Consumer<Personne> c = (Personne p) -> p.setAge(p.getAge() + 13);
		for(Personne p : lPersonne)
			c.accept(p);
		System.out.println(lPersonne);
	}
}

Ce qui nous donne :

[#Nom : toto - âge : 10#, #Nom : titi - âge : 20#, #Nom : tata - âge : 30#, #Nom : tutu - âge : 40#]
[#Nom : toto - âge : 23#, #Nom : titi - âge : 33#, #Nom : tata - âge : 43#, #Nom : tutu - âge : 53#]

Cette interface a aussi une méthode par défaut   andThen()  où nous pouvons appliquer encore d'autre choses. C'est simple non ? Alors continuons !

java.util.function.Predicate<T>

Je pense que vous avez compris le principe maintenant, donc vous devriez comprendre au premier coup d'oeil le code ci-dessous qui se contente de vérifier que l'objet Personne a un âge supérieur à 20 :

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

public class TestPredicate {

	public static void main(String[] args) {
		
		List<Personne> lPersonne = Arrays.asList(
				new Personne(10, "toto"),
				new Personne(20, "titi"),
				new Personne(30, "tata"),
				new Personne(40, "tutu")
		);
		
		Predicate<Personne> predicate = (Personne p) -> p.getAge() > 20;
		for (Personne p : lPersonne) {
			if(predicate.test(p))
				System.out.println(p.getNom() + " a l'âge requis !");
		}
	}
}

Avec comme sortie dans la console :

tata a l'âge requis !
tutu a l'âge requis !

java.util.function.Supplier<T>

Nous allons terminer ce chapitre par le plus facile (une fois n'est pas coutume...). Cette dernière se contente de retourner ce que nous lui avons demander de retourner...

import java.util.function.Supplier;

public class TestSupplier {
	public static void main(String[] args) {
		Supplier<String> s1 = () -> new String("hello !");
		System.out.println(s1.get());
		Supplier<Personne> s2 = () -> new Personne(50, "Dédé");
		System.out.println(s2.get());
	}
}

Ce code parle de lui-même. L'objet 's1' va systématiquement retourner un nouvel objet String et 's2' va, lui, systématiquement retourner un objet Personne comme le montre le résultat ci-dessous :

hello !
#Nom : Dédé - âge : 50#

Nous venons de voir comment utiliser certaines interface fonctionnelles présentes dans Java 8 avec les lambdas. Avant d'en terminer avec ce chapitre nous devons voir une autre façon d'exploiter ces interfaces fonctionnelles, grâce aux références de méthode.

Les références de méthodes

Une référence de méthode sert a définir une méthode abstraite d'une interface fonctionnelle. Cette magie opère grâce à un nouvel opérateur «   ::   ». La méthode qui va être implémenter dans l'interface fonctionnelle par ce biais ne devra pas être soumis à aucune ambiguïté et devra, vous l'aurez compris, correspondre à la signature de la méthode abstraite.
Pour simplifier au maximum, une référence de méthode est une lambda ultra simplifiée en utilisant ni plus ni moins qu'une méthode déjà existante dans une interface (méthode statique) classe (méthode statique ou constructeur) ou encore une méthode d'une instance de classe. Voici comment la syntaxe est faite :

« classe, interface ou instance » ::  « Nom de la méthode »

Tout cela semble barbare mais c'est très simple en fait, vous comprendrez beaucoup mieux avec l'exemple de code ci dessous (nous en profiterons pour voir d'autres interfaces fonctionnelles présentes dans Java 8)  :

import java.util.function.Consumer;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;

public class TestMethodsReference {

	public static void main(String[] args) {

		//Conversion d'un String en Double avec une référence à une méthode statique
		ToDoubleFunction<String> stringToDoubleLambda = (s) -> Double.parseDouble(s);
		ToDoubleFunction<String> stringToDoubleRef = Double::parseDouble;
		System.out.println(stringToDoubleLambda.applyAsDouble("0.1234567"));
		System.out.println(stringToDoubleRef.applyAsDouble("0.1234567"));
		
		//Utilisation d'une référence à une méthode d'instance (println)
		//de l'instance out de la classe 'System'
		Consumer<String> stringPrinterLambda = (s) -> System.out.println(s);
		Consumer<String> stringPrinterRef = System.out::println;
		stringPrinterLambda.accept("Bonjour !");
		stringPrinterRef.accept("Bonjour !");

		//Ici, nous utilisons carrément un constructeur
		//Notre interface fonctionnelle devient une fabrique d'Integer !
		ToIntFunction<String> testNew = Integer::new;
		Integer i = testNew.applyAsInt("1235");
		System.out.println("New Integer created : " + i.getClass());	
	}
}

Les commentaires se suffisent à eux-mêmes. Le code ci-dessus nous fournit la sortie suivante :

0.1234567
0.1234567
Bonjour !
Bonjour !
New Integer created : class java.lang.Integer

Ce chapitre touche maintenant à sa fin, j'espère que celui-ci ne vous a pas trop donner mal à la tête et que vous êtes prêt pour une autre nouveauté de Java 8, et pas des moindres : les streams !

En résumé

  • Les classes anonymes, les lambdas et les références de méthodes permettent de réduire la taille de votre code.

  • Les lambdas, les interfaces fonctionnelles, le package  java.util.function  et les références de méthode existent depuis Java 8.

  • Une interface fonctionnelle est une interface n'ayant qu'une seule méthode abstraite.

  • Une lambda permet de redéfinir une méthode d'une interface fonctionnelle très simplement de façon moins verbeuse qu'une classe anonyme.

  • Il y a de très forte chance que vous trouviez une interface fonctionnelle qui remplisse votre besoin dans le  java.util.function  .

Exemple de certificat de réussite
Exemple de certificat de réussite