W3docs

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");                  // 5

En 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 :

  • andThen se 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."
  • compose se lit de droite à gauche, comme la composition mathématique est écrite : f ∘ g signifie "appliquer g d'abord, puis f." 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 -> t

identity() 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=person

Sans 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 un int.
  • ToIntX — produit un int.
  • IntToLongXint en entrée, long en 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.

java— editable, runs on the server

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

  • trim.andThen(upper) et upper.compose(trim) ont produit le même String à partir de la même entrée. Ils ne diffèrent que par lequel se lit naturellement à l'endroit où vous l'écrivez — andThen correspond au flux de données de gauche à droite, compose correspond à la notation mathématique "f après g".
  • La chaîne plus longue trim.andThen(upper).andThen(length) a changé le type de sortie de String en Integer en cours de route. Le pipeline se compose de manière sûre ; le compilateur a suivi String -> String -> String -> Integer pour vous.
  • Function.identity() s'est intégré dans Collectors.toMap(Person::name, Function.identity()) comme mappeur de valeur. Le lambda p -> p aurait fonctionné, mais identity() 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 ; le IntUnaryOperator primitif 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 de BiFunction.compose — prétraiter deux entrées nécessiterait deux arguments compose, 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.

Pratique

Pratique
Vous avez `Function<String, String> trim = String::trim;` et `Function<String, Integer> length = String::length;`. Vous voulez une `Function<String, Integer>` qui coupe d'abord les espaces puis prend la longueur. Quelle expression la construit le plus naturellement ?
Vous avez `Function<String, String> trim = String::trim;` et `Function<String, Integer> length = String::length;`. Vous voulez une `Function<String, Integer>` qui coupe d'abord les espaces puis prend la longueur. Quelle expression la construit le plus naturellement ?
Was this page helpful?