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"); // 5Di 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:
andThensi legge da sinistra a destra, nello stesso ordine in cui scorrono i dati.trim.andThen(length).andThen(asString)è "trim, poi length, poi asString."composesi legge da destra a sinistra, come è scritta la composizione matematica:f ∘ gsignifica "applicagprima, poif."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 -> tidentity() 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=personSenza 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 unint.ToIntX— produce unint.IntToLongX—intin ingresso,longin 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.
Cosa osservare dall'esecuzione:
trim.andThen(upper)eupper.compose(trim)hanno prodotto la stessaStringdallo stesso input. Differiscono solo nel quale nome si legge naturalmente dove viene scritto —andThencorrisponde al flusso di dati da sinistra a destra,composecorrisponde alla notazione matematica "f dopo g".- La catena più lunga
trim.andThen(upper).andThen(length)ha cambiato il tipo di output daStringaIntegerlungo il percorso. La pipeline compone in modo type-safe; il compilatore ha tracciatoString -> String -> String -> Integerper te. Function.identity()si è inserito inCollectors.toMap(Person::name, Function.identity())come mapper del valore. La lambdap -> pavrebbe funzionato, maidentity()è la forma singleton senza allocazione e si legge come l'intento ("il valore è la persona").- La
Function<Integer, Integer>con boxing paga due boxing diIntegera ogni chiamata; laIntUnaryOperatorprimitiva 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 esisteBiFunction.compose— pre-elaborare due input richiederebbe due argomenticompose, 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.