Interfaces fonctionnelles Java 8

Bienvenue dans le tutoriel sur les exemples d’interfaces fonctionnelles de Java 8. Java a toujours été un langage de programmation orienté objet. Cela signifie que tout en programmation Java tourne autour des objets (à l’exception de certains types primitifs pour des raisons de simplicité). Nous n’avons pas seulement des fonctions en Java, elles font partie de la classe et nous devons utiliser la classe/objet pour invoquer n’importe quelle fonction.

Interfaces fonctionnelles de Java 8

Si nous examinons d’autres langages de programmation tels que C++, JavaScript; ils sont appelés langages de programmation fonctionnelle car nous pouvons écrire des fonctions et les utiliser lorsque nécessaire. Certains de ces langages prennent également en charge la programmation orientée objet ainsi que la programmation fonctionnelle. Être orienté objet n’est pas mauvais, mais cela apporte beaucoup de verbiage au programme. Par exemple, disons que nous devons créer une instance de Runnable. Habituellement, nous le faisons en utilisant des classes anonymes comme ci-dessous.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

Si vous regardez le code ci-dessus, la partie réellement utile est le code à l’intérieur de la méthode run(). Tout le reste du code est dû à la façon dont les programmes Java sont structurés. Les interfaces fonctionnelles de Java 8 et les expressions lambda nous aident à écrire un code plus petit et plus propre en supprimant beaucoup de code redondant.

Interface Fonctionnelle Java 8

Une interface avec exactement une méthode abstraite est appelée Interface Fonctionnelle. L’annotation @FunctionalInterface est ajoutée afin que nous puissions marquer une interface comme interface fonctionnelle. Il n’est pas obligatoire de l’utiliser, mais c’est une bonne pratique de l’utiliser avec les interfaces fonctionnelles pour éviter l’ajout accidentel de méthodes supplémentaires. Si l’interface est annotée avec l’annotation @FunctionalInterface et que nous essayons d’avoir plus d’une méthode abstraite, cela lance une erreur de compilation. Le principal avantage des interfaces fonctionnelles de Java 8 est que nous pouvons utiliser des expressions lambda pour les instancier et éviter d’utiliser une implémentation volumineuse de classe anonyme. L’API des collections Java 8 a été réécrite et la nouvelle API Stream est introduite qui utilise beaucoup d’interfaces fonctionnelles. Java 8 a défini beaucoup d’interfaces fonctionnelles dans le package java.util.function. Certaines des interfaces fonctionnelles utiles de Java 8 sont Consumer, Supplier, Function et Predicate. Vous pouvez trouver plus de détails à leur sujet dans Exemple de Flux Java 8. java.lang.Runnable est un excellent exemple d’interface fonctionnelle avec une seule méthode abstraite run(). Le code ci-dessous fournit quelques conseils pour les interfaces fonctionnelles:

interface Foo { boolean equals(Object obj); }
// Non fonctionnel car equals est déjà un membre implicite (classe Object)

interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// Fonctionnel car Comparator n'a qu'une seule méthode abstraite non-Object

interface Foo {
  int m();
  Object clone();
}
// Non fonctionnel car la méthode Object.clone n'est pas publique

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Fonctionnel : deux méthodes, mais elles ont la même signature

interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// Fonctionnel : Y.m est une sous-signature et substituable pour le type de retour

interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// Non fonctionnel : Aucune méthode n'a une sous-signature de toutes les méthodes abstraites

interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// Non fonctionnel : Aucune méthode n'a une sous-signature de toutes les méthodes abstraites

interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Erreur de compilation : aucune méthode n'est substituable pour le type de retour

interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// Erreur de compilation : signatures différentes, même écrasement

Expression Lambda

Les expressions lambda sont la façon dont nous pouvons visualiser la programmation fonctionnelle dans le monde orienté objet de Java. Les objets sont la base du langage de programmation Java et nous ne pouvons jamais avoir une fonction sans objet, c’est pourquoi le langage Java prend en charge l’utilisation des expressions lambda uniquement avec des interfaces fonctionnelles. Puisqu’il n’y a qu’une seule fonction abstraite dans les interfaces fonctionnelles, il n’y a pas de confusion dans l’application de l’expression lambda à la méthode. La syntaxe des expressions lambda est (argument) -> (corps). Maintenant, voyons comment nous pouvons écrire le Runnable anonyme ci-dessus en utilisant une expression lambda.

Runnable r1 = () -> System.out.println("My Runnable");

Essayons de comprendre ce qui se passe dans l’expression lambda ci-dessus.

  • Runnable est une interface fonctionnelle, c’est pourquoi nous pouvons utiliser une expression lambda pour créer son instance.
  • Étant donné que la méthode run() ne prend aucun argument, notre expression lambda n’a également aucun argument.
  • Tout comme les blocs if-else, nous pouvons éviter les accolades ({}) puisque nous avons une seule instruction dans le corps de la méthode. Pour plusieurs instructions, nous devrions utiliser des accolades comme pour n’importe quel autre méthode.

Pourquoi avons-nous besoin d’une expression lambda

  1. Réduction des lignes de code L’un des avantages évidents de l’utilisation d’une expression lambda est la réduction du nombre de lignes de code. Nous avons déjà vu comment nous pouvons facilement créer une instance d’une interface fonctionnelle en utilisant une expression lambda plutôt qu’en utilisant une classe anonyme.

  2. Prise en charge de l’exécution séquentielle et parallèle Un autre avantage de l’utilisation de l’expression lambda est que nous pouvons bénéficier du support des opérations séquentielles et parallèles de l’API Stream. Pour expliquer cela, prenons un exemple simple où nous devons écrire une méthode pour tester si un nombre passé est un nombre premier ou non. Traditionnellement, nous écririons son code comme ci-dessous. Le code n’est pas entièrement optimisé mais bon à des fins d’exemple, donc patientez avec moi là-dessus.

    //Approche traditionnelle
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i<number; i++){
    		if(number % i == 0) return false;
    	}
    	return true;
    }
    

    Le problème avec le code ci-dessus est qu’il est séquentiel de nature, si le nombre est très grand alors cela prendra un temps significatif. Un autre problème avec le code est qu’il y a tellement de points de sortie et il n’est pas lisible. Voyons comment nous pouvons écrire la même méthode en utilisant des expressions lambda et l’API Stream.

    //Approche déclarative
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream est une séquence d’éléments int primitifs prenant en charge des opérations d’agrégation séquentielles et parallèles. Il s’agit de la spécialisation primitive int de Stream. Pour plus de lisibilité, nous pouvons également écrire la méthode comme ci-dessous.

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    Si vous n’êtes pas familier avec IntStream, sa méthode range() retourne un IntStream ordonné séquentiellement de startInclusive (inclus) à endExclusive (exclus) par un pas incrémentiel de 1. La méthode noneMatch() retourne vrai si aucun élément de ce flux ne correspond au prédicat fourni. Il se peut qu’il n’évalue pas le prédicat sur tous les éléments s’il n’est pas nécessaire pour déterminer le résultat.

  3. Transmettre des comportements dans les méthodes Voyons comment nous pouvons utiliser les expressions lambda pour transmettre le comportement d’une méthode avec un exemple simple. Disons que nous devons écrire une méthode pour additionner les nombres dans une liste s’ils correspondent à un critère donné. Nous pouvons utiliser Predicate et écrire une méthode comme ci-dessous.

    public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) {
    	    return numbers.parallelStream()
    	    		.filter(predicate)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    Exemple d’utilisation :

    // somme de tous les nombres
    sumWithCondition(numbers, n -> true)
    // somme de tous les nombres pairs
    sumWithCondition(numbers, i -> i%2==0)
    // somme de tous les nombres supérieurs à 5
    sumWithCondition(numbers, i -> i>5)
    
  4. Une plus grande efficacité avec la paresse Un autre avantage de l’utilisation de l’expression lambda est l’évaluation paresseuse. Par exemple, supposons que nous devons écrire une méthode pour trouver le nombre impair maximal dans la plage de 3 à 11 et renvoyer son carré. Habituellement, nous écrirons du code pour cette méthode comme ceci:

    private static int trouverCarreDuMaxImpair(List<Integer> nombres) {
    		int max = 0;
    		for (int i : nombres) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

    Le programme ci-dessus s’exécutera toujours dans l’ordre séquentiel mais nous pouvons utiliser l’API Stream pour réaliser ceci et bénéficier de la recherche de la paresse. Voyons comment nous pouvons réécrire ce code de manière fonctionnelle en utilisant l’API Stream et les expressions lambda.

    public static int trouverCarreDuMaxImpair(List<Integer> nombres) {
    		return nombres.stream()
    				.filter(NumberTest::estImpair) 		// Le prédicat est une interface fonctionnelle et
    				.filter(NumberTest::estSuperieurA3)	// nous utilisons des lambdas pour l'initialiser
    				.filter(NumberTest::estInferieurA11)	// plutôt que des classes internes anonymes
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean estImpair(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean estSuperieurA3(int i){
    		return i > 3;
    	}
    	
    	public static boolean estInferieurA11(int i){
    		return i < 11;
    	}
    

    Si vous êtes surpris par l’opérateur double colon (::), il a été introduit en Java 8 et est utilisé pour les références de méthode. Le compilateur Java se charge de mapper les arguments à la méthode appelée. C’est une forme abrégée des expressions lambda i -> estSuperieurA3(i) ou i -> NumberTest.estSuperieurA3(i).

Exemples d’expressions lambda

Je fournis ci-dessous quelques extraits de code pour les expressions lambda avec de brefs commentaires les expliquant.

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body

// Bloc complexe avec plusieurs retours
() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

Références de méthodes et de constructeurs

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

C’est tout pour le tutoriel sur les Interfaces Fonctionnelles de Java 8 et les Expressions Lambda. Je recommande fortement de les utiliser car cette syntaxe est nouvelle en Java et il faudra un certain temps pour la maîtriser. Vous devriez également consulter les fonctionnalités de Java 8 pour découvrir toutes les améliorations et les changements de la version Java 8.

Source:
https://www.digitalocean.com/community/tutorials/java-8-functional-interfaces