W3docs

Java Function Interface

Trasforma un valore di un tipo in un altro in Java con l'interfaccia Function e i metodi andThen/compose.

Function<T, R> è l'interfaccia funzionale per la domanda "trasforma questo T in un R" — un input, un output, nessun effetto collaterale atteso. È la forma che accetta Stream.map, la forma che accetta Optional.map, la forma che accetta Map.computeIfAbsent, e la forma che accetta ogni metodo del JDK che dice "trasforma questo in quello". Un unico metodo astratto, tre o quattro metodi default utili, e una piccola algebra (andThen, compose, identity) per concatenare trasformazioni senza scrivere lambda intermedie.

L'interfaccia

@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) è il SAM (single abstract method). Ogni lambda o riferimento a metodo che si trova in una posizione Function<T, R> lo implementa.

Function<String, Integer> length = String::length;
int n = length.apply("hello");                  // 5

Di solito lascerai che sia stream.map(length) o optional.map(length) a chiamare apply per te. Conoscere il nome del metodo è importante quando scrivi codice che accetta una Function<T, R> e deve chiamarla una volta.

andThen e compose — due modi per concatenare

I due metodi default costruiscono una nuova Function concatenando il ricevitore con un'altra. Differiscono solo nella direzione:

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))

Entrambi costruiscono la stessa pipeline s -> length(trim(s)). La differenza è quale si legge meglio nel punto in cui viene scritto:

  • andThen si legge da sinistra a destra, nello stesso ordine in cui scorrono i dati. trim.andThen(length).andThen(asString) è "trim, poi length, poi asString."
  • compose si legge da destra a sinistra, come è scritta la composizione matematica: f ∘ g significa "applica g prima, poi f." length.compose(trim) è "length dopo trim."

Nel codice applicativo andThen è quasi sempre la scelta più chiara — il codice si legge dall'alto verso il basso, da sinistra a destra, e una pipeline da sinistra a destra corrisponde a questo. compose è utile quando hai una funzione finale e vuoi preporre una pre-elaborazione senza riscrivere la catena.

Entrambi sono lazy nel senso che non eseguono nulla al momento della composizione; producono semplicemente una nuova Function il cui apply chiama quelle sottostanti nel giusto ordine.

Function.identity() — la trasformazione no-op

Function<T, T> id = Function.identity();      // t -> t

identity() restituisce la stessa istanza a ogni chiamata (una lambda singleton), quindi ha costo di allocazione zero. L'unico posto dove si rivela utile è come mapper di chiave o valore in Collectors.toMap, dove devi passare una Function anche quando il valore è "l'elemento stesso":

Map<String, Person> byName = people.stream()
    .collect(Collectors.toMap(Person::name, Function.identity()));   // key=name, value=person

Senza Function.identity() scriveresti p -> p, che alloca una nuova lambda a ogni chiamata e si legge peggio.

Un punto sottile: identity() funziona solo quando i tipi di input e output sono gli stessi. Nel momento in cui un generico si allarga (Function<? super T, ? extends R>), il compilatore potrebbe costringerti a scrivere di nuovo una lambda. È un caso limite ma vale la pena conoscerlo quando l'inferenza di tipo generica si lamenta.

Function<T, R> versus UnaryOperator<T>

UnaryOperator<T> è la specializzazione per il caso in cui input e output sono dello stesso tipo:

UnaryOperator<String> upper = String::toUpperCase;       // String -> String
Function<String, String> sameShape = String::toUpperCase;

Entrambe sono istanze valide di Function<String, String>UnaryOperator<T> estende Function<T, T>. La differenza è a livello di API: List.replaceAll, Map.replaceAll e Comparator.thenComparing(UnaryOperator) dichiarano UnaryOperator<T> perché "sostituire ogni elemento con un valore dello stesso tipo trasformato" è esattamente quella forma. Passa un riferimento a metodo e il compilatore sceglierà quello giusto.

BiFunction<T, U, R> — due input

La forma a due argomenti:

BiFunction<String, Integer, String> repeat = String::repeat;
String s = repeat.apply("ab", 3);             // "ababab"

BiFunction ha lo stesso andThen ma nessun compose — l'asimmetria è intenzionale, perché pre-elaborare una funzione a due argomenti richiederebbe due parametri compose.

Il JDK usa BiFunction<K, V, V> per Map.merge e BiFunction<K, V, V_NEW> per Map.compute. BinaryOperator<T> è il caso speciale in cui tutti e tre i parametri di tipo sono T (input, input e output tutti uguali) — trattato nel capitolo BinaryOperator.

Specializzazioni primitive — tre famiglie

Function<Integer, String> esegue il boxing dell'int a ogni chiamata. Il package fornisce tre famiglie per evitarlo:

// 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 nomenclatura si legge come una frase:

  • IntX — opera su un int.
  • ToIntX — produce un int.
  • IntToLongXint in ingresso, long in uscita.

Stream.mapToInt(ToIntFunction) è il ponte da uno Stream<T> con boxing a un IntStream. Una volta su un IntStream, ogni trasformazione usa IntUnaryOperator o IntToLongFunction — e il costo del boxing rimane zero.

Un esempio pratico: composizione, identity e una specializzazione primitiva

Il programma seguente costruisce due Function, le compone con andThen e compose per mostrare che sono equivalenti, usa Function.identity() all'interno di un Collectors.toMap, e confronta una Function<Integer, Integer> con boxing con una IntUnaryOperator primitiva su un carico di lavoro abbastanza grande da sentire il costo del boxing.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • trim.andThen(upper) e upper.compose(trim) hanno prodotto la stessa String dallo stesso input. Differiscono solo nel quale nome si legge naturalmente dove viene scritto — andThen corrisponde al flusso di dati da sinistra a destra, compose corrisponde alla notazione matematica "f dopo g".
  • La catena più lunga trim.andThen(upper).andThen(length) ha cambiato il tipo di output da String a Integer lungo il percorso. La pipeline compone in modo type-safe; il compilatore ha tracciato String -> String -> String -> Integer per te.
  • Function.identity() si è inserito in Collectors.toMap(Person::name, Function.identity()) come mapper del valore. La lambda p -> p avrebbe funzionato, ma identity() è la forma singleton senza allocazione e si legge come l'intento ("il valore è la persona").
  • La Function<Integer, Integer> con boxing paga due boxing di Integer a ogni chiamata; la IntUnaryOperator primitiva non paga nulla. Una singola esecuzione riscaldata potrebbe mostrare tempi simili — il JIT è bravo a eliminare box di breve durata — ma sotto vera pressione di allocazione (heap grandi, GC concorrente, valori che escono) la variante primitiva è quella che regge. Usala nelle pipeline calde che gestiscono milioni di valori.
  • BiFunction.andThen(Function) ha concatenato una funzione a due argomenti con un follow-up a un argomento. Non esiste BiFunction.compose — pre-elaborare due input richiederebbe due argomenti compose, cosa che l'API evita deliberatamente.

Cosa viene dopo

Function<T, R> e Predicate<T> sono entrambe forme pure — input, output, nessun effetto collaterale atteso. Il capitolo successivo, Java Consumer e Supplier, tratta le due interfacce che escono da quella purezza: Consumer<T> prende un input e non produce nulla (un effetto collaterale — stampa, log, salvataggio), e Supplier<T> non prende nulla e produce un output (default lazy, factory, casualità). Completano la tassonomia a quattro angoli vista nella panoramica delle interfacce built-in.

Esercitazione

Pratica
Hai `Function<String, String> trim = String::trim;` e `Function<String, Integer> length = String::length;`. Vuoi una `Function<String, Integer>` che prima fa il trim e poi misura la lunghezza. Quale espressione la costruisce in modo più naturale?
Hai `Function<String, String> trim = String::trim;` e `Function<String, Integer> length = String::length;`. Vuoi una `Function<String, Integer>` che prima fa il trim e poi misura la lunghezza. Quale espressione la costruisce in modo più naturale?
Was this page helpful?