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 :
- Les méthodes
default— elles possèdent déjà un corps, donc un implémenteur n'a pas besoin d'en fournir un. - Les méthodes
static— elles appartiennent à l'interface elle-même, pas aux implémenteurs. - Les méthodes abstraites
publicqui surchargent une méthode dejava.lang.Object— par exempleequals,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 :
- 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.
- 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 queFunction<T, ValidationResult>, même si la forme correspond. - Vous avez besoin d'une exception vérifiée.
Function.applyne déclare aucune exception vérifiée ; si votre opération lève uneIOException, é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'tUne 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.
Ce qu'il faut retenir de l'exécution :
notBlank1(lambda),notBlank2(chaîne de références de méthodes) etnotBlank3(classe anonyme) implémentent tous la même interfaceFilter<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éthodesdefaultandetnegatesur l'interface constituent l'algèbre de composition — c'est pourquoi le JDK les ajoute àPredicate,FunctionetComparator.SafelyFunctional<T>déclare à la foisapply(T)etboolean equals(Object), et s'est quand même compilé avec@FunctionalInterface. La surchargeequalsest 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é
defaultdansFilter(transformant une méthode default en une deuxième méthode abstraite), l'annotation@FunctionalInterfaceforce 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.function — Function, Predicate, Consumer, Supplier, leurs variantes bi-, et les spécialisations primitives qui existent pour éviter le boxing.