Java - Les Structures fonctionnelles

Décembre 2016
Note : cette page présente des concepts introduits en Java 8 qui ne peuvent pas être utilisés dans les versions précédentes.

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éthode
fait 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.

A voir également :

Ce document intitulé «  Java - Les Structures fonctionnelles  » issu de CommentCaMarche (www.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.