Interfaces fonctionnelles intégrées de Java
Le package java.util.function — Function, Predicate, Consumer, Supplier et leurs variantes spécialisées.
Le package java.util.function livré avec Java 8 offre au JDK — et à votre code — un vocabulaire commun pour les lambdas. Sans lui, chaque méthode acceptant une fonction devrait définir sa propre interface ad hoc (StringMapper, IntToBool, RowHandler, …), et les lambdas définis pour l'une ne pourraient pas être réutilisés pour une autre. Le package résout ce problème avec 43 petites interfaces couvrant les formes qui reviennent sans cesse : « prendre une chose, en retourner une autre », « prendre une chose, décider oui ou non », « prendre une chose, faire quelque chose », « me donner une chose. »
Si vous n'apprenez que quatre interfaces de ce package, retenez Function, Predicate, Consumer et Supplier. Presque tout le reste n'est qu'une variante de l'une d'elles — des versions à deux arguments, des spécialisations primitives pour éviter le boxing, ou des utilitaires de composition.
Les quatre grandes interfaces
Function<T, R> f = t -> ...; // takes a T, returns an R — r = f.apply(t)
Predicate<T> p = t -> ...; // takes a T, returns a boolean — boolean b = p.test(t)
Consumer<T> c = t -> { ... }; // takes a T, returns nothing — c.accept(t)
Supplier<T> s = () -> ...; // takes nothing, returns a T — t = s.get()Chacune est annotée @FunctionalInterface et possède une méthode abstraite en un mot (apply, test, accept, get). Vous appellerez rarement ces méthodes directement avec les streams — stream().filter(predicate).map(function).forEach(consumer) s'en charge pour vous — mais connaître le nom de la méthode est important lorsque vous écrivez du code qui prend un Function<T, R> en paramètre et a besoin de l'invoquer.
Les formes correspondent aux questions courantes :
| Question | Interface |
|---|---|
| « Transformer un X en un Y ? » | Function<X, Y> |
| « Cet X est-il valide ? » | Predicate<X> |
| « Faire quelque chose avec ce X » | Consumer<X> |
| « Donne-moi un X » | Supplier<X> |
Variantes à deux arguments
Lorsque l'opération nécessite deux entrées, ajoutez le préfixe Bi :
BiFunction<T, U, R> f = (t, u) -> ...; // two ins, one out — apply
BiPredicate<T, U> p = (t, u) -> ...; // two ins, a boolean — test
BiConsumer<T, U> c = (t, u) -> { ... }; // two ins, no out — acceptIl n'existe pas de BiSupplier — Supplier ne prend aucun argument par définition, donc un « supplier à deux arguments » ne serait qu'une BiFunction.
Les variantes Bi correspondent exactement à ce qu'attendent Map.forEach((k, v) -> ...), Map.merge et Map.compute :
Map<String, Integer> scores = new HashMap<>();
scores.forEach((name, score) -> System.out.println(name + "=" + score)); // BiConsumer
scores.merge("alice", 1, Integer::sum); // BinaryOperator<Integer>BinaryOperator<T> est une BiFunction<T, T, T> — même type pour les deux entrées et la sortie. UnaryOperator<T> est de même une Function<T, T>.
Spécialisations primitives — éviter le coût du boxing
Function<Integer, Integer> fonctionne, mais chaque appel encapsule l'entrée et encapsule le résultat. Dans une boucle intensive, c'est un vrai coût. Le package propose donc des versions spécialisées pour les types primitifs :
IntFunction<R> f = i -> ...; // int in, R out
IntPredicate p = i -> ...; // int in, boolean out
IntConsumer c = i -> { ... }; // int in, void
IntSupplier s = () -> 42; // void in, int out
IntUnaryOperator u = i -> i * 2; // int in, int out
IntBinaryOperator b = (a, c2) -> a + c2;
ToIntFunction<T> f1 = t -> t.hashCode(); // T in, int out
ToIntBiFunction<T, U> f2 = (t, u) -> t.hashCode() + u.hashCode();
IntToLongFunction f3 = i -> (long) i * i; // int in, long out
IntToDoubleFunction f4 = i -> Math.sqrt(i);La même famille existe pour Long et Double. La convention de nommage se lit comme une phrase :
IntX— opère sur unint.ToIntX— produit unint.IntToLongX—inten entrée,longen sortie.
Dans le code de stream, mapToInt(...) retourne un IntStream, dont les opérations terminales (sum, average, min, max) retournent toutes des primitives sans boxing — ce qui représente l'un des gains pratiques les plus importants des variantes primitives.
La composition intégrée aux interfaces
La plupart des interfaces sont accompagnées de méthodes default qui permettent de composer sans écrire de nouveaux lambdas :
// Function: andThen (left-to-right), compose (right-to-left)
Function<String, String> trim = String::trim;
Function<String, Integer> len = String::length;
Function<String, Integer> trimLen = trim.andThen(len); // trim, then length
Function<String, Integer> sameThing = len.compose(trim); // length applied after trim
// Predicate: and / or / negate
Predicate<String> notNull = Objects::nonNull;
Predicate<String> notBlank = s -> !s.trim().isEmpty();
Predicate<String> useful = notNull.and(notBlank);
Predicate<String> blank = notBlank.negate();
// Consumer: andThen (run two consumers in sequence)
Consumer<String> log = System.out::println;
Consumer<String> save = s -> writeToFile(s);
Consumer<String> both = log.andThen(save);
// Comparator (in java.util, not java.util.function, but the same idea):
Comparator<Person> byName = Comparator.comparing(Person::name);
Comparator<Person> ordered = byName.thenComparing(Person::age);Il existe également une fabrique statique utile : Predicate.not(p) est un raccourci pour p.negate() et se lit plus naturellement à l'endroit de l'appel :
list.removeIf(Predicate.not(String::isBlank)); // remove all blank stringsFunction.identity et Predicate.isEqual — les petites méthodes statiques utiles
Deux méthodes de fabrique que vous verrez dans le code de stream et que vous devez reconnaître :
Function<T, T> id = Function.identity(); // t -> t — useful as a no-op map
Predicate<Object> isFoo = Predicate.isEqual("foo"); // o -> Objects.equals(o, "foo")Function.identity() est le plus souvent utilisé comme mappeur de clé ou de valeur dans Collectors.toMap :
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity()));Predicate.isEqual est rarement plus court que s -> s.equals("foo"), mais il compare avec Objects.equals de manière null-safe, ce qui compte lorsque le stream peut contenir null.
Un exemple concret : les quatre grandes interfaces, la composition et la spécialisation primitive
Le programme ci-dessous utilise Function, Predicate, Consumer et Supplier, en compose quelques-uns, et compare une Function<Integer, Integer> (avec boxing) à un IntUnaryOperator (primitif) en sommant une petite liste.
Ce qu'il faut retenir de l'exécution :
- Les quatre grandes interfaces correspondent clairement à quatre types de travail : transformer (
Function), tester (Predicate), agir (Consumer), produire (Supplier). Leurs noms de méthode abstraite (apply,test,accept,get) méritent d'être mémorisés. trim.andThen(length)etnotNull.and(notBlank)ont construit de nouvelles valeurs à partir d'anciennes sans aucune déclaration de méthode auxiliaire. C'est l'algèbre de composition que les interfaces embarquent sous forme de méthodesdefault.- La
Function<Integer, Integer>avec boxing est sensiblement plus lente que leIntUnaryOperatorprimitif, car chaque appel alloue deux objetsInteger. Dans les chemins critiques — les pipelines de stream traitant des millions de valeurs — les spécialisations primitives justifient leur existence. Predicate.not(notBlank)se lit plus naturellement quenotBlank.negate()à l'endroit d'un appelremoveIf. Les deux compilent vers la même chose.
La suite
Vous avez maintenant vu le vocabulaire standard. La question ergonomique restante avec les lambdas est « lorsque le corps du lambda délègue simplement à une méthode existante, peut-on l'écrire plus court ? » Oui — avec les références de méthode. Le chapitre suivant, Références de méthode Java, couvre l'opérateur :: et ses quatre formes (statique, instance liée, instance non liée, constructeur), et explique quand une référence de méthode est plus claire qu'un lambda et quand c'est l'inverse.