1. Approfondiamo l'interfaccia funzionale
Un'interfaccia funzionale è un'interfaccia in cui c'è ESATTAMENTE uno un metodo astratto (cioè non implementato). Proprio grazie a questo Java capisce: «ah, qui possiamo inserire una lambda!» oppure un riferimento a metodo.
Per evitare errori, in tali interfacce si usa di solito l’annotazione @FunctionalInterface. Non è obbligatoria, ma se la aggiungete e per sbaglio scrivete un secondo metodo astratto, il compilatore si lamenterà subito.
Esempio:
@FunctionalInterface
interface MyAction {
void run();
}
MyAction action = () -> System.out.println("Ciao dalla lambda!");
action.run(); // Stamperà: Ciao dalla lambda!
Perché serve?
- Permette di usare espressioni lambda e riferimenti a metodo al posto di creare classi anonime (meno boilerplate!).
- Fa capire al compilatore che l'interfaccia è destinata alla programmazione funzionale.
Curiosità: Nella libreria standard di Java ci sono già decine di tali interfacce — non serve reinventare la ruota!
2. Panoramica delle interfacce funzionali standard
Nel package java.util.function ci sono decine di interfacce funzionali. Vediamo le quattro più popolari (hanno la più alta «frequenza di utilizzo» tra tutte le interfacce Java).
| Interfaccia | Cosa accetta | Cosa restituisce | Uso tipico |
|---|---|---|---|
|
|
|
Verifica di una condizione (filtraggio) |
|
|
|
Eseguire un'azione sull'oggetto |
|
niente | |
Ottenere/generare un oggetto |
|
|
|
Trasformare T in R |
Predicate<T>
Descrizione: Funzione che accetta un oggetto di tipo T e restituisce true oppure false. Esempio tipico: filtrare una lista. Metodo chiave — test.
Predicate<String> isLong = s -> s.length() > 5;
System.out.println(isLong.test("Java")); // false
System.out.println(isLong.test("Functional")); // true
Consumer<T>
Descrizione: Accetta un oggetto di tipo T, esegue un'azione su di esso e non restituisce nulla. Metodo chiave — accept.
Consumer<String> printer = s -> System.out.println("Stampo: " + s);
printer.accept("Hello, world!"); // Stampo: Hello, world!
Supplier<T>
Descrizione: Non accetta nulla, restituisce un oggetto di tipo T. Lo si può vedere come un «generatore» di valori. Metodo chiave — get.
Supplier<Double> randomSupplier = () -> Math.random();
System.out.println(randomSupplier.get()); // Ad esempio, 0.1234567
Function<T, R>
Descrizione: Accetta un oggetto di tipo T e restituisce un oggetto di tipo R. Esempio tipico: trasformazione dei dati. Metodo chiave — apply.
Function<String, Integer> stringToLength = s -> s.length();
System.out.println(stringToLength.apply("Java")); // 4
In breve: UnaryOperator, BinaryOperator, BiFunction
- UnaryOperator<T> — equivalente a Function<T, T>: accetta e restituisce lo stesso tipo.
- BinaryOperator<T> — equivalente a BiFunction<T, T, T>: accetta due T, restituisce un T.
- BiFunction<T, U, R> — accetta due tipi diversi, restituisce un terzo.
UnaryOperator<Integer> square = x -> x * x;
BinaryOperator<Integer> sum = (a, b) -> a + b;
BiFunction<String, Integer, String> repeat = (s, n) -> s.repeat(n);
3. Esempi d'uso
Vediamo come queste interfacce compaiono in problemi reali e, in particolare, nelle collection e nello Stream API.
Passaggio ai metodi delle collection e dello Stream API
Esempio 1: Predicate e filtraggio
List<String> words = List.of("java", "stream", "lambda", "code");
List<String> longWords = words.stream()
.filter(word -> word.length() > 4) // Predicate<String>
.toList();
System.out.println(longWords); // [stream, lambda]
Esempio 2: Consumer e forEach
words.forEach(word -> System.out.println("Parola: " + word)); // Consumer<String>
Esempio 3: Function e map
List<Integer> lengths = words.stream()
.map(word -> word.length()) // Function<String, Integer>
.toList();
System.out.println(lengths); // [4, 6, 6, 4]
Esempio 4: Supplier e generazione di valori
Supplier<String> greetingSupplier = () -> "Ciao, Java!";
System.out.println(greetingSupplier.get()); // Ciao, Java!
Confronto con le classi anonime
Prima si scriveva così:
Predicate<String> isShort = new Predicate<String>() {
@Override
public boolean test(String s) {
return s.length() < 5;
}
};
Con le lambda è diventato molto più piacevole:
Predicate<String> isShort = s -> s.length() < 5;
4. Pratica: scriviamo espressioni lambda per ogni interfaccia
Implementiamo una piccola applicazione — un elenco di utenti. Ogni utente sarà rappresentato dalla classe User:
public class User {
private final String name;
private final int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
Creiamo un elenco di utenti:
List<User> users = List.of(
new User("Anna", 23),
new User("Boris", 17),
new User("Vika", 31),
new User("Gosha", 15)
);
Predicate: filtrare gli adulti
Predicate<User> isAdult = user -> user.getAge() >= 18;
List<User> adults = users.stream()
.filter(isAdult)
.toList();
System.out.println("Adulti: " + adults); // Adulti: [Anna (23), Vika (31)]
Consumer: stampa degli utenti
Consumer<User> printUser = user -> System.out.println("Utente: " + user);
adults.forEach(printUser);
Supplier: generazione di utenti
Supplier<User> randomUserSupplier = () -> {
String[] names = {"Dima", "Katya", "Lyosha"};
int randomAge = 10 + (int)(Math.random() * 30);
String randomName = names[(int)(Math.random() * names.length)];
return new User(randomName, randomAge);
};
User randomUser = randomUserSupplier.get();
System.out.println("Utente casuale: " + randomUser);
Function: ottenere il nome dell'utente
Function<User, String> getName = user -> user.getName();
List<String> names = users.stream()
.map(getName)
.toList();
System.out.println("Nomi: " + names); // Nomi: [Anna, Boris, Vika, Gosha]
5. Dettagli utili
Uso nello Stream API: filter, map, forEach e altro
Mettiamo tutto insieme e scriviamo una pipeline di trasformazioni:
users.stream()
.filter(user -> user.getAge() >= 18) // Predicate<User>
.map(user -> user.getName().toUpperCase()) // Function<User, String>
.forEach(name -> System.out.println("Adulto: " + name)); // Consumer<String>
Risultato:
Adulto: ANNA
Adulto: VIKA
Tabella promemoria: cosa inserire dove
| Dove si usa | Quale interfaccia serve | Esempio d'uso |
|---|---|---|
| filter (Stream) | |
|
| map (Stream) | |
|
| forEach (Stream, List) | |
|
| generate (Stream) | |
|
Perché è importante conoscere le interfacce funzionali?
- Sono alla base di tutte le espressioni lambda in Java.
- Permettono di scrivere codice universale, riutilizzabile e conciso.
- Semplificano il lavoro con collection, stream e attività asincrone.
Quando usare quale interfaccia?
- Predicate — quando bisogna verificare una condizione (filtrare, cercare).
- Consumer — quando bisogna fare qualcosa con l’oggetto (stampare, scrivere, inviare).
- Supplier — quando bisogna ottenere o generare un oggetto (factory, generatori).
- Function — quando bisogna trasformare un oggetto da un tipo a un altro.
6. Errori tipici con le interfacce funzionali
Errore n. 1: scelta errata dell'interfaccia. A volte i principianti confondono Predicate e Function — ad esempio, provano a restituire boolean da Function invece che da Predicate. Ricordate: Predicate restituisce sempre boolean, Function — qualsiasi altro tipo.
Errore n. 2: non usare le interfacce standard. Spesso si scrivono interfacce proprie come «Checker» con il metodo boolean check(T t) invece di usare Predicate. Meglio usare quelle standard — sono supportate ovunque e rendono il codice più chiaro per gli altri sviluppatori.
Errore n. 3: la lambda è troppo complessa. Se una lambda diventa un mini-romanzo da 10 righe, conviene estrarla in un metodo o una classe separati. La lambda è sinonimo di brevità e leggibilità.
Errore n. 4: annotazione @FunctionalInterface dimenticata. Se scrivete una vostra interfaccia funzionale — non dimenticate l’annotazione. Vi proteggerà da errori accidentali (come aggiungere un secondo metodo astratto).
Errore n. 5: uso di stato mutabile all'interno della lambda. Se una lambda modifica variabili esterne o collection, ciò può portare a bug inattesi, soprattutto lavorando con thread. Meglio evitare effetti collaterali.
GO TO FULL VERSION