Interfacce Funzionali in Java
Interfacce con un solo metodo astratto in Java, usate come target per lambda, annotate con @FunctionalInterface.
Un'interfaccia funzionale è un'interfaccia con esattamente un metodo astratto. Quel metodo è il target a cui viene compilata una lambda o un riferimento a metodo. Runnable, Comparator<T>, Callable<V>, Supplier<T>, Function<T, R>, Predicate<T>, Consumer<T>, ActionListener, FileFilter — sono tutte interfacce funzionali. Nel JDK ne esistono già decine e potrai scriverne di tue quando nessuna di esse si adatta.
Il capitolo precedente ha mostrato lambda come () -> 42 e s -> s.length() che "vengono compilate all'interfaccia richiesta dal contesto." Questo capitolo risponde alla domanda cosa rende un'interfaccia un target valido — la regola del metodo astratto singolo (SAM) — e come @FunctionalInterface ti permette di dire "sì, questa lo è, e voglio che il compilatore lo verifichi."
La regola SAM, in dettaglio
Per essere funzionale, un'interfaccia deve dichiarare esattamente un metodo che richiede un'implementazione. La formulazione è importante: non "esattamente un metodo in totale," ma "esattamente un metodo astratto." Tre categorie di metodi non contano ai fini di questo unico requisito:
- Metodi
default— hanno già un corpo, quindi chi la implementa non deve fornirne uno. - Metodi
static— appartengono all'interfaccia stessa, non a chi la implementa. - Metodi
publicastratti che sovrascrivono un metodo dijava.lang.Object— ad es.equals,hashCode,toString. Ogni classe eredita già le implementazioni daObject, quindi ridichiarli in un'interfaccia non aggiunge un nuovo requisito.
Il terzo punto sorprende molti. Comparator<T> dichiara boolean equals(Object), ma è comunque funzionale perché quel metodo proviene da Object. Il vero metodo astratto è int compare(T, T).
@FunctionalInterface
interface MyComparator<T> {
int compare(T a, T b); // the one SAM
boolean equals(Object other); // Object override — doesn't count
default MyComparator<T> reversed() { // default — doesn't count
return (a, b) -> compare(b, a);
}
static <T extends Comparable<T>> MyComparator<T> natural() { // static — doesn't count
return (a, b) -> a.compareTo(b);
}
}@FunctionalInterface — verifica a tempo di compilazione opt-in
L'annotazione è facoltativa. Un'interfaccia è funzionale in base alla sua struttura, non in base alla presenza dell'annotazione. Annotarla ti offre però due vantaggi:
- Errore di compilazione se l'interfaccia smette di essere funzionale. Se aggiungi accidentalmente un secondo metodo astratto, il compilatore ti blocca subito — sull'interfaccia, non in ogni punto di chiamata che la usa come target lambda.
- Documentazione. L'annotazione segnala "questa è pensata per essere usata come target lambda," il che vale la pena specificare per qualsiasi caso non ovvio.
@FunctionalInterface
interface Validator<T> {
boolean isValid(T value);
boolean isInvalid(T value); // <-- compile error: not a functional interface
}Senza l'annotazione, il secondo metodo trasformerebbe silenziosamente Validator<T> in un'interfaccia non funzionale, e il primo punto di chiamata in stile lambda che la usa fallirebbe la compilazione con un messaggio confuso lontano dalla causa.
L'annotazione è anche la convenzione per le interfacce funzionali del JDK — Function, Predicate, Consumer, Supplier, Runnable, Callable la portano tutte.
Lambda, riferimenti a metodo e classi anonime sono intercambiabili
Un'interfaccia funzionale accetta tre tipi di valore, liberamente intercambiabili tra loro:
Predicate<String> blank1 = s -> s.trim().isEmpty(); // lambda
Predicate<String> blank2 = String::isBlank; // method reference (since Java 11)
Predicate<String> blank3 = new Predicate<>() { // anonymous class
@Override public boolean test(String s) { return s.trim().isEmpty(); }
};Tutti e tre implementano la stessa interfaccia Predicate<String> e producono valori equivalenti al punto di chiamata. La lambda e il riferimento a metodo sono decisamente più brevi; la classe anonima è riservata ai rari casi elencati nel capitolo precedente (più di un metodo necessario, stato locale al metodo, this che fa riferimento alla nuova istanza).
Interfacce funzionali generiche
L'interfaccia può essere parametrizzata — è così che una singola dichiarazione di Function<T, R> può essere usata per ogni trasformazione:
@FunctionalInterface
interface Mapper<T, R> {
R map(T input);
}
Mapper<String, Integer> length = s -> s.length();
Mapper<Integer, String> hex = n -> Integer.toHexString(n);I parametri possono avere limiti, possono includere più variabili di tipo e possono essere riutilizzati tra le interfacce — la libreria standard usa ogni variazione.
Scrivere la propria interfaccia funzionale
La maggior parte delle volte dovresti usare le interfacce predefinite in java.util.function — il prossimo capitolo le esplora tutte. Scrivi la tua quando:
- La semantica merita un nome.
Validator<T>si legge meglio in un punto di chiamata rispetto aFunction<T, ValidationResult>anche se la struttura coincide. - Hai bisogno di un'eccezione checked.
Function.applynon dichiara eccezioni checked; se la tua operazione lanciaIOException, scrivi un SAM che la dichiari. - La struttura non è nella libreria standard. Un metodo che prende tre argomenti (una funzione tri-aria) non ha un'interfaccia predefinita — scrivine una quando ne hai bisogno.
@FunctionalInterface
interface IOFunction<T, R> {
R apply(T input) throws IOException;
}
IOFunction<Path, String> readAll = Files::readString; // declared exception — built-in Function can'tUna quantità sorprendentemente elevata di "devo scrivere questa?" dipende da leggibilità o propagazione delle eccezioni.
I metodi default guadagnano il loro posto
Il caso in cui scriverai la tua interfaccia funzionale e aggiungerai anche metodi default è quando vuoi che i chiamanti possano comporre istanze:
@FunctionalInterface
interface Filter<T> {
boolean keep(T value);
default Filter<T> and(Filter<T> other) {
return v -> keep(v) && other.keep(v);
}
default Filter<T> negate() {
return v -> !keep(v);
}
}
Filter<Integer> positive = n -> n > 0;
Filter<Integer> even = n -> n % 2 == 0;
Filter<Integer> posOdd = positive.and(even.negate());Questa è esattamente la ricetta che il JDK usa per Predicate.and / or / negate, Function.andThen / compose e Comparator.thenComparing. Il metodo astratto singolo è il comportamento; i metodi default sono l'algebra di composizione che lo circonda.
Un esempio pratico: scrivere, annotare e comporre
Il programma seguente definisce un'interfaccia funzionale Filter<T> con due metodi default, dimostra la regola SAM (un metodo astratto aggiuntivo non compilerebbe) e mostra lambda, riferimenti a metodo e una classe anonima che implementano tutti lo stesso SAM.
Cosa osservare dall'esecuzione:
notBlank1(lambda),notBlank2(catena di riferimenti a metodo) enotBlank3(classe anonima) implementano tutte la stessa interfacciaFilter<String>— in modo intercambiabile. La lambda è la più concisa; la classe anonima è riservata ai casi che le lambda non possono gestire.positive.and(even.negate())ha composto tre filtri in uno senza dichiarazioni di metodo aggiuntive. I metodidefaultandenegatesull'interfaccia sono l'algebra di composizione — ecco perché il JDK li aggiunge aPredicate,FunctioneComparator.SafelyFunctional<T>dichiara siaapply(T)cheboolean equals(Object), ed è comunque compilata con@FunctionalInterface. L'override diequalsè ereditato daObject, quindi non conta ai fini della regola del metodo astratto singolo.- Se rimuovi la parola chiave
defaultinFilter(trasformando un default in un secondo metodo astratto), l'annotazione@FunctionalInterfaceforza un errore di compilazione immediato alla dichiarazione dell'interfaccia — molto prima che qualsiasi punto di chiamata lambda veda messaggi di inferenza confusi.
Cosa fare dopo
Sai riconoscere un'interfaccia funzionale, scriverne una quando il JDK non ha ciò di cui hai bisogno e lasciare che il compilatore ne verifichi la struttura. Quasi sempre, però, la risposta giusta è "usa ciò che è già disponibile." Il prossimo capitolo, Java Built-in Functional Interfaces, esplora java.util.function — Function, Predicate, Consumer, Supplier, le loro varianti bi-argomento e le specializzazioni primitive che esistono per evitare il boxing.