Interface fonctionnelle Function en Java
Transformez une valeur d'un type en un autre en Java avec l'interface Function et les méthodes andThen/compose.
Function<T, R> est l'interface fonctionnelle pour la question "transformer ce T en R" — une entrée, une sortie, aucun effet de bord attendu. C'est la forme qu'accepte Stream.map, la forme qu'accepte Optional.map, la forme qu'accepte Map.computeIfAbsent, et la forme que toute méthode du JDK qui dit "transformer ceci en cela" attend. Une seule méthode abstraite, trois ou quatre méthodes par défaut utiles, et une petite algèbre (andThen, compose, identity) pour enchaîner des transformations sans écrire de lambdas intermédiaires.
L'interface
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); // the only abstract method
default <V> Function<V, R> compose(Function<? super V, ? extends T> before);
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after);
static <T> Function<T, T> identity();
}apply(T) est la SAM (single abstract method). Chaque lambda ou référence de méthode qui se retrouve dans une position Function<T, R> l'implémente.
Function<String, Integer> length = String::length;
int n = length.apply("hello"); // 5En général, vous laisserez stream.map(length) ou optional.map(length) appeler apply pour vous. Connaître le nom de la méthode importe lorsque vous écrivez du code qui accepte une Function<T, R> et doit l'appeler une fois.
andThen et compose — deux façons d'enchaîner
Les deux méthodes par défaut construisent une nouvelle Function en enchaînant le receveur avec une autre. Elles ne diffèrent que par la direction :
Function<String, String> trim = String::trim;
Function<String, Integer> length = String::length;
Function<String, Integer> trimThenLength = trim.andThen(length); // f.andThen(g): g(f(x))
Function<String, Integer> sameThing = length.compose(trim); // g.compose(f): g(f(x))Les deux construisent le même pipeline s -> length(trim(s)). La différence réside dans laquelle se lit le mieux au point d'appel :
andThense lit de gauche à droite, dans le même ordre que le flux de données.trim.andThen(length).andThen(asString)signifie "trim, puis length, puis asString."composese lit de droite à gauche, comme la composition mathématique est écrite :f ∘ gsignifie "appliquergd'abord, puisf."length.compose(trim)signifie "length après trim."
Dans le code applicatif, andThen est presque toujours le choix le plus clair — le code se lit de haut en bas, de gauche à droite, et un pipeline de gauche à droite correspond à cela. compose est utile lorsque vous avez une fonction finale et souhaitez préfixer un prétraitement sans réécrire la chaîne.
Les deux sont paresseux dans le sens où ils n'exécutent rien au moment de la composition ; ils produisent simplement une nouvelle Function dont apply appelle les sous-jacentes dans le bon ordre.
Function.identity() — la transformation sans effet
Function<T, T> id = Function.identity(); // t -> tidentity() retourne la même instance à chaque appel (un lambda singleton), donc son coût d'allocation est nul. L'unique endroit où il est vraiment utile est en tant que mappeur de clé ou de valeur dans Collectors.toMap, où vous devez passer une Function même quand la valeur est "l'élément lui-même" :
Map<String, Person> byName = people.stream()
.collect(Collectors.toMap(Person::name, Function.identity())); // key=name, value=personSans Function.identity(), vous écririez p -> p, ce qui alloue un nouveau lambda à chaque appel et se lit moins bien.
Un point subtil : identity() ne fonctionne que lorsque les types d'entrée et de sortie sont identiques. Dès qu'un générique s'élargit (Function<? super T, ? extends R>), le compilateur peut vous forcer à épeler à nouveau un lambda. C'est un cas limite, mais utile à connaître quand l'inférence générique se plaint.
Function<T, R> versus UnaryOperator<T>
UnaryOperator<T> est la spécialisation pour le cas où les types d'entrée et de sortie sont le même type :
UnaryOperator<String> upper = String::toUpperCase; // String -> String
Function<String, String> sameShape = String::toUpperCase;Les deux sont des instances Function<String, String> valides — UnaryOperator<T> étend Function<T, T>. La différence se situe au niveau de l'API : List.replaceAll, Map.replaceAll et Comparator.thenComparing(UnaryOperator) déclarent UnaryOperator<T> car "remplacer chaque élément par une valeur transformée du même type" correspond exactement à cette forme. Passez une référence de méthode et le compilateur choisit la bonne.
BiFunction<T, U, R> — deux entrées
La forme à deux arguments :
BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3); // "ababab"BiFunction possède le même andThen mais pas de compose — l'asymétrie est délibérée, car prétraiter une fonction à deux arguments nécessiterait deux paramètres compose.
Le JDK utilise BiFunction<K, V, V> pour Map.merge et BiFunction<K, V, V_NEW> pour Map.compute. BinaryOperator<T> est le cas spécial où les trois paramètres de type sont T (entrée, entrée et sortie identiques) — traité dans le chapitre BinaryOperator.
Spécialisations primitives — trois familles
Function<Integer, String> encapsule l'int à chaque appel. Le package fournit trois familles pour éviter cela :
// 1. Primitive in, object out — "IntFunction<R>"
IntFunction<String> fromInt = i -> "n=" + i;
// 2. Object in, primitive out — "ToIntFunction<T>"
ToIntFunction<String> strLen = String::length;
ToDoubleFunction<Item> price = Item::price;
// 3. Primitive in, primitive out — "IntToLongFunction", "IntUnaryOperator", etc.
IntToLongFunction square = i -> (long) i * i;
IntUnaryOperator doubleIt = i -> i * 2;
DoubleUnaryOperator halve = d -> d / 2.0;La dénomination se lit comme une phrase :
IntX— opère sur unint.ToIntX— produit unint.IntToLongX—inten entrée,longen sortie.
Stream.mapToInt(ToIntFunction) est le pont d'un Stream<T> encapsulé vers un IntStream. Une fois sur un IntStream, chaque transformation utilise IntUnaryOperator ou IntToLongFunction — et le coût d'encapsulation reste nul.
Un exemple concret : composition, identity et une spécialisation primitive
Le programme ci-dessous construit deux Functions, les compose avec andThen et compose pour montrer qu'elles sont équivalentes, utilise Function.identity() dans un Collectors.toMap, et compare une Function<Integer, Integer> encapsulée avec un IntUnaryOperator primitif sur une charge de travail suffisamment grande pour ressentir le coût de l'encapsulation.
Ce qu'il faut retenir de l'exécution :
trim.andThen(upper)etupper.compose(trim)ont produit le mêmeStringà partir de la même entrée. Ils ne diffèrent que par lequel se lit naturellement à l'endroit où vous l'écrivez —andThencorrespond au flux de données de gauche à droite,composecorrespond à la notation mathématique "f après g".- La chaîne plus longue
trim.andThen(upper).andThen(length)a changé le type de sortie deStringenIntegeren cours de route. Le pipeline se compose de manière sûre ; le compilateur a suiviString -> String -> String -> Integerpour vous. Function.identity()s'est intégré dansCollectors.toMap(Person::name, Function.identity())comme mappeur de valeur. Le lambdap -> paurait fonctionné, maisidentity()est la forme singleton sans allocation et exprime clairement l'intention ("la valeur est la personne").- La
Function<Integer, Integer>encapsulée paie pour deux encapsulations d'Integerà chaque appel ; leIntUnaryOperatorprimitif ne paie rien. Une seule exécution préchauffée peut montrer des timings similaires — le JIT est efficace pour éliminer les encapsulations de courte durée — mais sous une vraie pression d'allocation (grands tas, GC concurrent, valeurs qui s'échappent) le variant primitif est celui qui tient. Utilisez-le dans les pipelines critiques qui traitent des millions de valeurs. BiFunction.andThen(Function)a enchaîné une fonction à deux arguments avec une suite à un argument. Il n'y a pas deBiFunction.compose— prétraiter deux entrées nécessiterait deux argumentscompose, ce que l'API évite délibérément.
Prochaine étape
Function<T, R> et Predicate<T> sont toutes deux des formes pures — entrée, sortie, aucun effet de bord attendu. Le chapitre suivant, Java Consumer et Supplier, couvre les deux interfaces qui sortent de cette pureté : Consumer<T> prend une entrée et ne produit rien (un effet de bord — afficher, journaliser, stocker), et Supplier<T> ne prend rien et produit une sortie (valeur par défaut paresseuse, fabrique, aléatoire). Ils complètent la taxonomie à quatre coins vue dans l'aperçu des interfaces intégrées.