Java - Les Structures fonctionnelles
Note : cette page présente des concepts introduits en Java 8 qui ne peuvent pas être utilisés dans les versions précédentes.
Une interface fonctionnelle est une interface qui ne définit qu'une seule méthode à implémenter.
Une telle interface devrait désormais posséder l'annotation @FunctionalInterface, même si elle est facultative afin d'assurer la rétro-compatibilité avec les codes développés avant Java 8.
Exemple (en Java 8)
Remarque : avec l'apparition dans Java 8 des méthodes par défaut, il est possible d'avoir une interface avec plusieurs méthodes par défaut. Une interface reste néanmoins fonctionnelle tant qu'il y a une et une seule méthode à implémenter.
Jusqu'à Java 7, pour utiliser une interface, il était nécessaire de définir une classe qui implémente cette interface et instancier un objet de cette classe (parfois au travers de classes anonymes).
Exemple (en Java 5+)
Avec les interfaces fonctionnelles il est désormais possible de simplifier l'écriture de ces interfaces. En effet, en partant du principe qu'il n'y a qu'une seule méthode, il n'est plus vraiment utile d'en rappeler son nom.
Les lambdas expressions en Java 8 permettent donc d'écrire :
La syntaxe
Si le calcul se fait sur une seule ligne comme dans notre exemple, on peut enlever les accolades et avoir directement :
Lorsqu'il n'y a pas d'ambiguïtés quant au type de données manipulées, il est même possible de laisser Java s'occuper du typage des variables. On pourra donc encore simplifier :
Il est ainsi possible de rédiger plus clairement son code, lorsqu'au travers d'une méthode un des paramètres est une interface fonctionnelle.
Exemple (en Java 8)
Lorsque le code est un peu long ou utilisé plusieurs fois, il faut organiser son code. Pour les interfaces avant Java 8, il s'agissait de créer une nouvelle classe pour chaque implémentation.
Exemple (en Java 5+)
Comme dans notre exemple, la plupart des implémentations des interfaces fonctionnelles, n'avaient pas d'attributs privés, n'utilisait que le constructeur par défaut et ne servait jamais de l'héritage implicite d'Object.
Finalement, une méthode static nous suffirait et c'est ce qu'il est possible de faire avec les références de méthodes :
Exemple (en Java 8)
La notation
On peut généraliser les références de méthodes aux constructeurs, en utilisant la "méthode" new, ou à n'importe quelle méthode non static en la liant à un objet particulier.
Exemples
Remarque : les interfaces Supplier<T>, Function<T,R>, ainsi qu'une quarantaine d'autres sont spécifiées dans le package java.util.function, accompagnées de méthodes par défaut permettant leur manipulation.
Puisque l'on ne manipule plus des objets, mais directement des méthodes, Java opère des optimisations dans le code.
Exemple :
Des différentes écritures présentées, il est préférable d'utiliser
Ainsi Java pourra modifier le code à l'exécution et faire :
Pseudo code (à l'exécution) :
En revanche, ce dernier code est déconseillé :
En effet Java serait obligé pour traiter ces instructions de créer une nouvelle classe qui hérite de Object et implémente Comparator, de l'instancier et passer l'objet en paramètre de la méthode, qui ne pourrait pas être optimisé.
Donc même si on gagne en clarté du code grâce à l'écriture lambda de l'interface fonctionnelle, il ne faut pas perdre de vue que l'intérêt est surtout dans la création et le passage de méthode par références.
Interface fonctionnelle
Une interface fonctionnelle est une interface qui ne définit qu'une seule méthode à implémenter.
Une telle interface devrait désormais posséder l'annotation @FunctionalInterface, même si elle est facultative afin d'assurer la rétro-compatibilité avec les codes développés avant Java 8.
Exemple (en Java 8)
@FunctionalInterface public interface Comparator<T> { // résultat <0 si o1<o2, =0 si o1=o2 et >0 si o1>o2 int compare(T t1, T t2); }
Remarque : avec l'apparition dans Java 8 des méthodes par défaut, il est possible d'avoir une interface avec plusieurs méthodes par défaut. Une interface reste néanmoins fonctionnelle tant qu'il y a une et une seule méthode à implémenter.
Expressions Lambda
Jusqu'à Java 7, pour utiliser une interface, il était nécessaire de définir une classe qui implémente cette interface et instancier un objet de cette classe (parfois au travers de classes anonymes).
Exemple (en Java 5+)
Comparator<Integer> comp = new Comparator<Integer>() { @Override public int compare(Integer i1, Integer i2) { return i1-i2; } }
Avec les interfaces fonctionnelles il est désormais possible de simplifier l'écriture de ces interfaces. En effet, en partant du principe qu'il n'y a qu'une seule méthode, il n'est plus vraiment utile d'en rappeler son nom.
Les lambdas expressions en Java 8 permettent donc d'écrire :
Comparator<Integer> comp = (Integer i1, Integer i2) -> { return i1-i2; }
La syntaxe
(Type paramètre, ...) -> { corps }définit une fonction lambda.
Si le calcul se fait sur une seule ligne comme dans notre exemple, on peut enlever les accolades et avoir directement :
Comparator<Integer> comp = (Integer i1, Integer i2) -> i1-i2;
Lorsqu'il n'y a pas d'ambiguïtés quant au type de données manipulées, il est même possible de laisser Java s'occuper du typage des variables. On pourra donc encore simplifier :
Comparator<Integer> comp = (i1, i2) -> i1-i2;
Il est ainsi possible de rédiger plus clairement son code, lorsqu'au travers d'une méthode un des paramètres est une interface fonctionnelle.
Exemple (en Java 8)
List<Integer> list = Arrays.asList(7,4,1,8,5,2,9,6,3); // tri en ordre croissant Collections.sort(list, (a,b) -> a-b); System.out.println(list); // [1, 2, 3, 4, 5, 6, 7, 8, 9] // tri en ordre décroissant Collections.sort(list, (a,b) -> b-a); System.out.println(list); // [9, 8, 7, 6, 5, 4, 3, 2, 1]
Références de méthodes
Lorsque le code est un peu long ou utilisé plusieurs fois, il faut organiser son code. Pour les interfaces avant Java 8, il s'agissait de créer une nouvelle classe pour chaque implémentation.
Exemple (en Java 5+)
public class Test { public static class AscendingIntegerComparator implements Comparator<Integer> { @Override public int compare(Integer i1, Integer i2) { return i1 - i2; } } public static void main(String[] args) { List<Integer> list = Arrays.asList(7,4,1,8,5,2,9,6,3); Collections.sort(list, new AscendingIntegerComparator()); System.out.println(list); } }
Comme dans notre exemple, la plupart des implémentations des interfaces fonctionnelles, n'avaient pas d'attributs privés, n'utilisait que le constructeur par défaut et ne servait jamais de l'héritage implicite d'Object.
Finalement, une méthode static nous suffirait et c'est ce qu'il est possible de faire avec les références de méthodes :
Exemple (en Java 8)
public class Test { public static int ascendingIntegerComparator( Integer i1, Integer i2) { return i1 - i2; } public static void main(String[] args) { List<Integer> list = Arrays.asList(7,4,1,8,5,2,9,6,3); Collections.sort(list, Test::ascendingIntegerComparator); System.out.println(list); } }
La notation
classe :: méthodefait donc référence à une méthode statique. Dans notre exemple le second paramètre de sort (de type Comparator<T>) n'est donc plus un objet, mais une méthode !
On peut généraliser les références de méthodes aux constructeurs, en utilisant la "méthode" new, ou à n'importe quelle méthode non static en la liant à un objet particulier.
Exemples
@FunctionalInterface public interface Supplier<T> { T get(); } @FunctionalInterface public interface Function<T, R> { R apply(T t); } public static void main(String[] args) { Function<Integer, List<String>> fun = ArrayList::new; List<String> list = fun.apply(3); // new ArrayList(3) Supplier<Integer> size = list::size; System.out.println(size.get()); // list.size() = 0 list.add("toto"); System.out.println(size.get()); // list.size() = 1 }
Remarque : les interfaces Supplier<T>, Function<T,R>, ainsi qu'une quarantaine d'autres sont spécifiées dans le package java.util.function, accompagnées de méthodes par défaut permettant leur manipulation.
Plus qu'une nouvelle syntaxe
Puisque l'on ne manipule plus des objets, mais directement des méthodes, Java opère des optimisations dans le code.
Exemple :
public static boolean isGreater(Integer a, Integer b, Comparator<Integer> comp) { return comp.compare(a,b) > 0; }
Des différentes écritures présentées, il est préférable d'utiliser
isGreater(x, y, (a,b) -> a-b);ou
isGreater(x, y, Test::ascendingIntegerComparator);.
Ainsi Java pourra modifier le code à l'exécution et faire :
Pseudo code (à l'exécution) :
public static boolean isGreater(x, y, (a,b) -> a-b) { return (x-y) > 0; }
public static boolean isGreater(x, y, Test::ascendingIntegerComparator) { return Test.ascendingIntegerComparator(x, y) > 0; }
En revanche, ce dernier code est déconseillé :
Comparator<Integer> comp = (a,b) -> a-b; boolean greater = isGreater(x, y, comp);
En effet Java serait obligé pour traiter ces instructions de créer une nouvelle classe qui hérite de Object et implémente Comparator, de l'instancier et passer l'objet en paramètre de la méthode, qui ne pourrait pas être optimisé.
Donc même si on gagne en clarté du code grâce à l'écriture lambda de l'interface fonctionnelle, il ne faut pas perdre de vue que l'intérêt est surtout dans la création et le passage de méthode par références.