W3docs

Interfaces fonctionnelles Java

Interfaces à méthode abstraite unique en Java, cibles des lambdas, annotées avec @FunctionalInterface.

Une interface fonctionnelle est une interface possédant exactement une méthode abstraite. C'est cette méthode unique vers laquelle se compile un lambda ou une référence de méthode. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — toutes sont des interfaces fonctionnelles. Il en existe des dizaines dans le JDK et vous écrirez les vôtres lorsqu'aucune ne correspond à votre besoin.

Le chapitre précédent présentait des lambdas comme () -> 42 et s -> s.length() qui « se compilent vers l'interface que le contexte exige ». Ce chapitre répond à la question : qu'est-ce qui rend une interface une cible valide — la règle de la méthode abstraite unique (SAM, single-abstract-method) — et comment @FunctionalInterface vous permet de déclarer « oui, c'en est une, et je veux que le compilateur l'impose. »

La règle SAM, en détail

Pour être fonctionnelle, une interface doit déclarer exactement une méthode qui nécessite une implémentation. La formulation est importante : pas « exactement une méthode au total », mais « exactement une méthode abstraite ». Trois catégories de méthodes ne comptent pas dans ce décompte :

  1. Les méthodes default — elles possèdent déjà un corps, donc un implémenteur n'a pas besoin d'en fournir un.
  2. Les méthodes static — elles appartiennent à l'interface elle-même, pas aux implémenteurs.
  3. Les méthodes abstraites public qui surchargent une méthode de java.lang.Object — par exemple equals, hashCode, toString. Toute classe hérite déjà d'implémentations d'Object, donc les re-déclarer dans une interface n'ajoute pas de nouvelle exigence.

Le troisième point surprend souvent. Comparator<T> déclare boolean equals(Object), mais reste fonctionnelle car cette méthode provient d'Object. La vraie méthode abstraite est int compare(T, T).

@FunctionalInterface
interface MyComparator<T> {
  int compare(T a, T b);                          // the one SAM
  boolean equals(Object other);                   // Object override — doesn't count
  default MyComparator<T> reversed() {            // default — doesn't count
    return (a, b) -> compare(b, a);
  }
  static <T extends Comparable<T>> MyComparator<T> natural() {   // static — doesn't count
    return (a, b) -> a.compareTo(b);
  }
}

@FunctionalInterface — vérification à la compilation facultative

L'annotation est optionnelle. Une interface est fonctionnelle en fonction de sa forme, pas de sa présence. Mais l'annoter apporte deux avantages :

  1. Erreur de compilation si l'interface cesse d'être fonctionnelle. Ajoutez accidentellement une deuxième méthode abstraite et le compilateur vous arrête immédiatement — au niveau de l'interface, pas à chaque site d'appel qui l'utilise comme cible de lambda.
  2. Documentation. L'annotation signale « ceci est destiné à être utilisé comme cible de lambda », ce qui vaut la peine d'être précisé dans tout cas non évident.
@FunctionalInterface
interface Validator<T> {
  boolean isValid(T value);
  boolean isInvalid(T value);     // <-- compile error: not a functional interface
}

Sans l'annotation, cette deuxième méthode transformerait silencieusement Validator<T> en interface non fonctionnelle, et le premier site d'appel utilisant un lambda échouerait à la compilation avec un message déroutant, loin de la cause.

L'annotation est aussi la convention des interfaces fonctionnelles du JDK lui-même — Function, Predicate, Consumer, Supplier, Runnable, Callable la portent toutes.

Lambdas, références de méthodes et classes anonymes sont interchangeables

Une interface fonctionnelle accepte trois types de valeurs, librement interchangeables :

Predicate<String> blank1 = s -> s.trim().isEmpty();               // lambda
Predicate<String> blank2 = String::isBlank;                        // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() {                    // anonymous class
  @Override public boolean test(String s) { return s.trim().isEmpty(); }
};

Les trois implémentent la même interface Predicate<String> et produisent des valeurs équivalentes au site d'appel. Le lambda et la référence de méthode sont nettement plus courts ; la classe anonyme est réservée aux rares cas présentés dans le chapitre précédent (plus d'une méthode nécessaire, état local à la méthode, this renvoyant à la nouvelle instance).

Interfaces fonctionnelles génériques

L'interface peut être paramétrée — c'est ainsi qu'une seule déclaration de Function<T, R> peut être utilisée pour toutes les transformations :

@FunctionalInterface
interface Mapper<T, R> {
  R map(T input);
}

Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex    = n -> Integer.toHexString(n);

Les paramètres peuvent être bornés, inclure plusieurs variables de type et être réutilisés entre interfaces — la bibliothèque standard exploite toutes les variantes.

Écrire votre propre interface fonctionnelle

La plupart du temps, vous devriez utiliser les interfaces intégrées de java.util.function — le chapitre suivant les passe toutes en revue. Écrivez les vôtres quand :

  • La sémantique mérite un nom. Validator<T> se lit mieux à un site d'appel que Function<T, ValidationResult>, même si la forme correspond.
  • Vous avez besoin d'une exception vérifiée. Function.apply ne déclare aucune exception vérifiée ; si votre opération lève une IOException, écrivez un SAM qui la déclare.
  • La forme n'existe pas dans la bibliothèque standard. Une méthode prenant trois arguments (une tri-fonction) n'a pas d'interface intégrée — écrivez-en une lorsque vous en avez besoin.
@FunctionalInterface
interface IOFunction<T, R> {
  R apply(T input) throws IOException;
}

IOFunction<Path, String> readAll = Files::readString;       // declared exception — built-in Function can't

Une quantité étonnante de décisions « devrais-je écrire ceci ? » se résume soit à la lisibilité, soit à la propagation d'exceptions.

Les méthodes default justifient leur présence

Le seul endroit où vous écrirez votre propre interface fonctionnelle et ajouterez des méthodes default est lorsque vous souhaitez que les appelants puissent composer des instances :

@FunctionalInterface
interface Filter<T> {
  boolean keep(T value);

  default Filter<T> and(Filter<T> other) {
    return v -> keep(v) && other.keep(v);
  }
  default Filter<T> negate() {
    return v -> !keep(v);
  }
}

Filter<Integer> positive = n -> n > 0;
Filter<Integer> even     = n -> n % 2 == 0;
Filter<Integer> posOdd   = positive.and(even.negate());

C'est exactement la recette qu'utilise le JDK pour Predicate.and / or / negate, Function.andThen / compose et Comparator.thenComparing. La méthode abstraite unique est le comportement ; les méthodes default sont l'algèbre de composition qui l'entoure.

Exemple complet : écrire, annoter, composer

Le programme ci-dessous définit une interface fonctionnelle Filter<T> avec deux méthodes default, démontre la règle SAM (une méthode abstraite supplémentaire ne compilerait pas), et montre des lambdas, des références de méthodes et une classe anonyme implémentant tous le même SAM.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • notBlank1 (lambda), notBlank2 (chaîne de références de méthodes) et notBlank3 (classe anonyme) implémentent tous la même interface Filter<String> — de façon interchangeable. Le lambda est le plus court ; la classe anonyme est réservée aux cas que les lambdas ne peuvent pas gérer.
  • positive.and(even.negate()) a composé trois filtres en un seul sans aucune déclaration de méthode supplémentaire. Les méthodes default and et negate sur l'interface constituent l'algèbre de composition — c'est pourquoi le JDK les ajoute à Predicate, Function et Comparator.
  • SafelyFunctional<T> déclare à la fois apply(T) et boolean equals(Object), et s'est quand même compilé avec @FunctionalInterface. La surcharge equals est héritée d'Object, donc elle ne compte pas contre la règle de la méthode abstraite unique.
  • Si vous supprimez un mot-clé default dans Filter (transformant une méthode default en une deuxième méthode abstraite), l'annotation @FunctionalInterface force une erreur de compilation immédiate à la déclaration de l'interface — bien avant que les sites d'appel de lambdas ne voient des échecs d'inférence déroutants.

La suite

Vous savez reconnaître une interface fonctionnelle, en écrire une lorsque le JDK n'a pas ce qu'il vous faut, et demander au compilateur d'en imposer la forme. La plupart du temps, cependant, la bonne réponse est « utilisez ce qui existe déjà ». Le chapitre suivant, Interfaces fonctionnelles intégrées de Java, parcourt java.util.functionFunction, Predicate, Consumer, Supplier, leurs variantes bi-, et les spécialisations primitives qui existent pour éviter le boxing.

Exercices

Pratique
Une interface déclare trois méthodes : une méthode abstraite, une méthode `default`, et `boolean equals(Object)` re-déclarée depuis `Object`. Est-ce un `@FunctionalInterface` valide ?
Une interface déclare trois méthodes : une méthode abstraite, une méthode `default`, et `boolean equals(Object)` re-déclarée depuis `Object`. Est-ce un `@FunctionalInterface` valide ?
Was this page helpful?