W3docs

Interfacce Funzionali Built-in di Java

Il package java.util.function — Function, Predicate, Consumer, Supplier e le loro varianti specializzate.

Il package java.util.function è stato introdotto con Java 8 per fornire al JDK — e al tuo codice — un vocabolario condiviso per le lambda. Senza di esso, ogni metodo che accettava una funzione avrebbe dovuto definire la propria interfaccia ad hoc (StringMapper, IntToBool, RowHandler, …), e le lambda definite per una non potevano essere riutilizzate per un'altra. Il package risolve questo problema con 43 piccole interfacce che coprono le forme che si ripresentano continuamente: "prendi una cosa, restituiscine un'altra", "prendi una cosa, decidi sì o no", "prendi una cosa, fai qualcosa", "dammi una cosa".

Se impari solo quattro interfacce da questo package, impara Function, Predicate, Consumer e Supplier. Quasi tutto il resto è una variante di una di esse — versioni a due argomenti, specializzazioni primitive per evitare il boxing, o helper per la composizione.

Le quattro principali

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

Ognuna è annotata con @FunctionalInterface e ha un metodo astratto con un nome di una parola (apply, test, accept, get). Raramente chiamerai quei metodi direttamente quando si lavora con gli stream — stream().filter(predicate).map(function).forEach(consumer) esegue la chiamata al posto tuo — ma conoscere il nome del metodo è importante quando scrivi codice che accetta un Function<T, R> come parametro e deve invocarlo.

Le forme corrispondono a domande comuni:

DomandaInterfaccia
"Trasformare una X in una Y?"Function<X, Y>
"Questa X è valida?"Predicate<X>
"Fare qualcosa con questa X"Consumer<X>
"Dammi una X"Supplier<X>

Varianti a due argomenti

Quando l'operazione richiede due input, aggiungi il prefisso 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                     — accept

Non esiste BiSupplierSupplier per definizione non prende argomenti, quindi un "supplier a due argomenti" sarebbe semplicemente una BiFunction.

Le varianti Bi sono esattamente ciò che Map.forEach((k, v) -> ...), Map.merge e Map.compute si aspettano:

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> è una BiFunction<T, T, T> — stesso tipo per entrambi gli input e per l'output. UnaryOperator<T> è analogamente una Function<T, T>.

Specializzazioni primitive — evitare il costo del boxing

Function<Integer, Integer> funziona, ma ogni chiamata esegue il boxing dell'input e dell'output. In un ciclo stretto questo ha un costo reale. Il package offre quindi versioni specializzate per i tipi primitivi:

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 stessa famiglia esiste per Long e Double. La convenzione di denominazione si legge come una frase:

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

Nel codice con gli stream, mapToInt(...) restituisce un IntStream, le cui operazioni terminali (sum, average, min, max) restituiscono tutte valori primitivi senza boxing — che è uno dei maggiori vantaggi pratici delle varianti primitive.

Composizione integrata nelle interfacce

La maggior parte delle interfacce include metodi default che consentono di comporre senza scrivere nuove lambda:

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

Esiste anche un utile factory statico: Predicate.not(p) è una scorciatoia per p.negate() e si legge in modo più naturale nel punto di chiamata:

list.removeIf(Predicate.not(String::isBlank));    // remove all blank strings

Function.identity e Predicate.isEqual — i piccoli statici utili

Due metodi factory che incontrerai nel codice con gli stream e che dovresti riconoscere:

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() è usato più spesso come mapper di chiavi o valori in Collectors.toMap:

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

Predicate.isEqual raramente è più breve di s -> s.equals("foo"), ma confronta in modo null-safe tramite Objects.equals, il che è importante quando lo stream può contenere null.

Un esempio pratico: le quattro principali, composizione e specializzazione primitiva

Il programma qui sotto utilizza Function, Predicate, Consumer e Supplier, ne compone alcune e confronta una Function<Integer, Integer> (con boxing) con un IntUnaryOperator (primitiva) sommando una piccola lista.

java— editable, runs on the server

Cosa ricavare dall'esecuzione:

  • Le quattro interfacce principali si mappano chiaramente su quattro tipi di lavoro: trasformare (Function), verificare (Predicate), agire (Consumer), produrre (Supplier). Vale la pena memorizzare i nomi dei loro metodi astratti (apply, test, accept, get).
  • trim.andThen(length) e notNull.and(notBlank) hanno costruito nuovi valori da quelli esistenti senza dichiarare metodi helper. Questa è l'algebra di composizione che le interfacce portano come metodi default.
  • La Function<Integer, Integer> con boxing è significativamente più lenta dell'IntUnaryOperator primitiva perché ogni chiamata alloca due oggetti Integer. Nei percorsi critici — pipeline di stream che elaborano milioni di valori — le specializzazioni primitive si giustificano pienamente.
  • Predicate.not(notBlank) si legge in modo più naturale di notBlank.negate() in un punto di chiamata removeIf. Entrambi compilano nella stessa cosa.

Cosa viene dopo

Hai ora visto il vocabolario standard. La domanda rimanente sull'ergonomia delle lambda è "quando il corpo della lambda si limita a delegare a un metodo esistente, posso scriverlo in modo più conciso?" Sì — con i riferimenti a metodi. Il prossimo capitolo, Riferimenti a Metodi di Java, tratta l'operatore :: e le sue quattro forme (statico, istanza vincolata, istanza non vincolata, costruttore), e spiega quando un riferimento a metodo è più chiaro di una lambda e quando è il contrario.

Esercitazione

Pratica
Un metodo dichiara `void each(Consumer<String> action)`. Quali dei seguenti sono argomenti validi?
Un metodo dichiara `void each(Consumer<String> action)`. Quali dei seguenti sono argomenti validi?
Was this page helpful?