Expressions lambda et interfaces fonctionnelles: conseils et meilleures pratiques

Expressions lambda et interfaces fonctionnelles: conseils et meilleures pratiques

1. Vue d'ensemble

Maintenant que Java 8 est utilisé à grande échelle, des modèles et des meilleures pratiques ont commencé à émerger pour certaines de ses fonctionnalités principales. Dans ce tutoriel, nous examinerons de plus près les interfaces fonctionnelles et les expressions lambda.

Lectures complémentaires:

Pourquoi les variables locales utilisées dans Lambda doivent-elles être définitives ou réellement définitives?

Découvrez pourquoi Java nécessite que les variables locales soient effectivement définitives lorsqu'elles sont utilisées dans un lambda.

Read more

Java 8 - Comparaison puissante avec Lambdas

Sort Elegant in Java 8 - Les expressions lambda vont bien au-delà du sucre syntaxique et apportent une puissante sémantique fonctionnelle en Java.

Read more

2. Préférez les interfaces fonctionnelles standard

Les interfaces fonctionnelles, qui sont rassemblées dans le packagejava.util.function, satisfont la plupart des besoins des développeurs en fournissant des types de cibles pour les expressions lambda et les références de méthodes. Chacune de ces interfaces est générale et abstraite, ce qui les rend faciles à adapter à presque toutes les expressions lambda. Les développeurs doivent explorer ce package avant de créer de nouvelles interfaces fonctionnelles.

Considérons une interfaceFoo:

@FunctionalInterface
public interface Foo {
    String method(String string);
}

et une méthodeadd() dans une classeUseFoo, qui prend cette interface comme paramètre:

public String add(String string, Foo foo) {
    return foo.method(string);
}

Pour l'exécuter, vous écririez:

Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);

Regardez de plus près et vous verrez queFoo n'est rien de plus qu'une fonction qui accepte un argument et produit un résultat. Java 8 fournit déjà une telle interface dansFunction<T,R> à partir du packagejava.util.function.

Maintenant, nous pouvons supprimer complètement l'interfaceFoo et changer notre code en:

public String add(String string, Function fn) {
    return fn.apply(string);
}

Pour l'exécuter, nous pouvons écrire:

Function fn =
  parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);

3. Utilisez l'annotation@FunctionalInterface

Annotez vos interfaces fonctionnelles avec@FunctionalInterface. Au début, cette annotation semble inutile. Même sans cela, votre interface sera considérée comme fonctionnelle à condition qu’elle ne contienne qu’une méthode abstraite.

Mais imaginez un grand projet avec plusieurs interfaces - il est difficile de tout contrôler manuellement. Une interface conçue pour être fonctionnelle pourrait être modifiée accidentellement en ajoutant une ou plusieurs méthodes abstraites, la rendant inutilisable en tant qu'interface fonctionnelle.

Mais en utilisant l'annotation@FunctionalInterface, le compilateur déclenchera une erreur en réponse à toute tentative de rupture de la structure prédéfinie d'une interface fonctionnelle. C'est également un outil très pratique pour rendre votre architecture d'application plus facile à comprendre pour les autres développeurs.

Alors, utilisez ceci:

@FunctionalInterface
public interface Foo {
    String method();
}

au lieu de juste:

public interface Foo {
    String method();
}

4. Ne pas abuser des méthodes par défaut dans les interfaces fonctionnelles

Vous pouvez facilement ajouter des méthodes par défaut à l'interface fonctionnelle. Ceci est acceptable pour le contrat d'interface fonctionnelle tant qu'il n'y a qu'une seule déclaration de méthode abstraite:

@FunctionalInterface
public interface Foo {
    String method();
    default void defaultMethod() {}
}

Les interfaces fonctionnelles peuvent être étendues par d'autres interfaces fonctionnelles si leurs méthodes abstraites ont la même signature. Par exemple:

@FunctionalInterface
public interface FooExtended extends Baz, Bar {}

@FunctionalInterface
public interface Baz {
    String method();
    default void defaultBaz() {}
}

@FunctionalInterface
public interface Bar {
    String method();
    default void defaultBar() {}
}

Tout comme pour les interfaces classiques, l’extension de différentes interfaces fonctionnelles avec la même méthode par défaut peut être problématique. Par exemple, supposons que les interfacesBar etBaz ont toutes deux une méthode par défautdefaultCommon(). Dans ce cas, vous obtiendrez une erreur de compilation:

interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...

Pour résoudre ce problème, la méthodedefaultCommon() doit être remplacée dans l'interfaceFoo. Vous pouvez bien sûr fournir une implémentation personnalisée de cette méthode. Mais si vous souhaitez utiliser l'une des implémentations de l'interface parent (par exemple, à partir de l'interfaceBaz), ajoutez la ligne de code suivante dans le corps de la méthodedefaultCommon():

Baz.super.defaultCommon();

Mais fais attention. Adding too many default methods to the interface is not a very good architectural decision. Il doit être considéré comme un compromis, à utiliser uniquement lorsque cela est nécessaire, pour mettre à niveau les interfaces existantes sans interrompre la compatibilité descendante.

5. Instancier des interfaces fonctionnelles avec des expressions Lambda

Le compilateur vous permettra d'utiliser une classe interne pour instancier une interface fonctionnelle. Cependant, cela peut conduire à un code très prolixe. Vous devriez préférer les expressions lambda:

Foo foo = parameter -> parameter + " from Foo";

sur une classe intérieure:

Foo fooByIC = new Foo() {
    @Override
    public String method(String string) {
        return string + " from Foo";
    }
};

The lambda expression approach can be used for any suitable interface from old libraries. Il est utilisable pour des interfaces telles queRunnable,Comparator, etc. However, thisdoesn’t mean that you should review your whole older codebase and change everything.

6. Évitez de surcharger les méthodes avec des interfaces fonctionnelles comme paramètres

Utilisez des méthodes avec des noms différents pour éviter les collisions; Regardons un exemple:

public interface Processor {
    String process(Callable c) throws Exception;
    String process(Supplier s);
}

public class ProcessorImpl implements Processor {
    @Override
    public String process(Callable c) throws Exception {
        // implementation details
    }

    @Override
    public String process(Supplier s) {
        // implementation details
    }
}

À première vue, cela semble raisonnable. Mais toute tentative d’exécution de l’une des méthodes deProcessorImpl:

String result = processor.process(() -> "abc");

se termine par une erreur avec le message suivant:

reference to process is ambiguous
both method process(java.util.concurrent.Callable)
in com.example.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier)
in com.example.java8.lambda.tips.ProcessorImpl match

Pour résoudre ce problème, nous avons deux options. The first is to use methods with different names:

String processWithCallable(Callable c) throws Exception;

String processWithSupplier(Supplier s);

The second is to perform casting manually. Ce n'est pas préféré.

String result = processor.process((Supplier) () -> "abc");

7. Ne considérez pas les expressions Lambda comme des classes internes

Malgré notre exemple précédent, où nous substituions essentiellement une classe lambda à la classe inner, les deux concepts diffèrent de manière importante: la portée.

Lorsque vous utilisez une classe interne, une nouvelle portée est créée. Vous pouvez masquer les variables locales de la portée englobante en instanciant de nouvelles variables locales portant le même nom. Vous pouvez également utiliser le mot-cléthis dans votre classe interne comme référence à son instance.

Cependant, les expressions lambda fonctionnent avec une portée englobante. Vous ne pouvez pas masquer les variables de la portée englobante à l’intérieur du corps de lambda. Dans ce cas, le mot-cléthis est une référence à une instance englobante.

Par exemple, dans la classeUseFoo vous avez une variable d'instancevalue:

private String value = "Enclosing scope value";

Ensuite, dans une méthode de cette classe, placez le code suivant et exécutez cette méthode.

public String scopeExperiment() {
    Foo fooIC = new Foo() {
        String value = "Inner class value";

        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String resultIC = fooIC.method("");

    Foo fooLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = fooLambda.method("");

    return "Results: resultIC = " + resultIC +
      ", resultLambda = " + resultLambda;
}

Si vous exécutez la méthodescopeExperiment(), vous obtiendrez le résultat suivant:Results: resultIC = Inner class value, resultLambda = Enclosing scope value

Comme vous pouvez le voir, en appelantthis.value dans IC, vous pouvez accéder à une variable locale depuis son instance. Mais dans le cas du lambda, l'appelthis.value vous donne accès à la variablevalue qui est définie dans la classeUseFoo, mais pas à la variablevalue définie à l'intérieur du le corps de lambda.

8. Gardez les expressions Lambda courtes et explicites

Si possible, utilisez des constructions d'une ligne au lieu d'un gros bloc de code. Souvenez-vous delambdas should be anexpression, not a narrative. Malgré sa syntaxe concise,lambdas should precisely express the functionality they provide.

Il s’agit principalement de conseils stylistiques, car les performances ne changeront pas radicalement. En général, cependant, il est beaucoup plus facile de comprendre et de travailler avec un tel code.

Cela peut être réalisé de plusieurs manières - voyons de plus près.

8.1. Évitez les blocs de code dans le corps de Lambda

Dans une situation idéale, les lambdas devraient être écrits dans une ligne de code. Avec cette approche, le lambda est une construction auto-explicative, qui déclare quelle action doit être exécutée avec quelles données (dans le cas de lambdas avec des paramètres).

Si vous avez un gros bloc de code, la fonctionnalité du lambda n'est pas immédiatement claire.

Dans cet esprit, procédez comme suit:

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

au lieu de:

Foo foo = parameter -> { String result = "Something " + parameter;
    //many lines of code
    return result;
};

However, please don’t use this “one-line lambda” rule as dogma. Si vous avez deux ou trois lignes dans la définition de lambda, il peut ne pas être utile d'extraire ce code dans une autre méthode.

8.2. Évitez de spécifier des types de paramètres

Dans la plupart des cas, un compilateur est capable de résoudre le type de paramètres lambda à l'aide detype inference. Par conséquent, l'ajout d'un type aux paramètres est facultatif et peut être omis.

Faire ceci:

(a, b) -> a.toLowerCase() + b.toLowerCase();

au lieu de cela:

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

8.3. Éviter les parenthèses autour d'un seul paramètre

La syntaxe Lambda nécessite des parenthèses uniquement autour de plusieurs paramètres ou en l'absence de paramètre. C'est pourquoi il est prudent de raccourcir un peu votre code et d'exclure les parenthèses lorsqu'il n'y a qu'un seul paramètre.

Alors, fais ceci:

a -> a.toLowerCase();

au lieu de cela:

(a) -> a.toLowerCase();

8.4. Évitez la déclaration de retour et les accolades

Les instructionsBraces etreturn sont facultatives dans les corps lambda sur une ligne. Cela signifie qu'ils peuvent être omis pour plus de clarté et de concision.

Faire ceci:

a -> a.toLowerCase();

au lieu de cela:

a -> {return a.toLowerCase()};

8.5. Utiliser les références de méthode

Bien souvent, même dans nos exemples précédents, les expressions lambda appellent simplement des méthodes déjà implémentées ailleurs. Dans cette situation, il est très utile d'utiliser une autre fonctionnalité Java 8:method references.

Donc, l'expression lambda:

a -> a.toLowerCase();

pourrait être remplacé par:

String::toLowerCase;

Ce n'est pas toujours plus court, mais cela rend le code plus lisible.

9. Utiliser des variables «effectivement finales»

L'accès à une variable non finale dans les expressions lambda provoquera l'erreur de compilation. But it doesn’t mean that you should mark every target variable as final.

Selon le concept «effectively final», un compilateur traite chaque variable commefinal, tant qu'elle n'est affectée qu'une seule fois.

Il est prudent d’utiliser ces variables dans lambdas car le compilateur contrôlera leur état et déclenchera une erreur lors de la compilation immédiatement après toute tentative de modification.

Par exemple, le code suivant ne compilera pas:

public void method() {
    String localVariable = "Local";
    Foo foo = parameter -> {
        String localVariable = parameter;
        return localVariable;
    };
}

Le compilateur vous informera que:

Variable 'localVariable' is already defined in the scope.

Cette approche devrait simplifier le processus de sécurisation de l’exécution de lambda.

10. Protéger les variables d'objet de la mutation

L'un des principaux objectifs des lambdas est leur utilisation dans le calcul parallèle - ce qui signifie qu'ils sont vraiment utiles en matière de sécurité des threads.

Le paradigme «effectivement final» aide beaucoup ici, mais pas dans tous les cas. Lambdas ne peut pas changer la valeur d'un objet de la portée englobante. Mais dans le cas de variables d'objet modifiables, un état peut être modifié dans les expressions lambda.

Considérons le code suivant:

int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();

Ce code est légal, car la variabletotal reste «effectivement définitive». Mais l'objet référencé aura-t-il le même état après l'exécution du lambda? No!

Conservez cet exemple à titre de rappel pour éviter le code susceptible de provoquer des mutations inattendues.

11. Conclusion

Dans ce didacticiel, nous avons vu quelques bonnes pratiques et pièges dans les expressions lambda et les interfaces fonctionnelles de Java 8. Malgré l'utilité et la puissance de ces nouvelles fonctionnalités, ce ne sont que des outils. Chaque développeur doit faire attention en les utilisant.

Lesource code complet de l'exemple est disponible dansthis GitHub project - il s'agit d'un projet Maven et Eclipse, il peut donc être importé et utilisé tel quel.