CodeGym /Corsi /JAVA 25 SELF /Interfacce funzionali: Predicate, Consumer, Supplier, Fun...

Interfacce funzionali: Predicate, Consumer, Supplier, Function

JAVA 25 SELF
Livello 49 , Lezione 0
Disponibile

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
Predicate<T>
T
boolean
Verifica di una condizione (filtraggio)
Consumer<T>
T
void
Eseguire un'azione sull'oggetto
Supplier<T>
niente
T
Ottenere/generare un oggetto
Function<T, R>
T
R
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)
Predicate<T>
filter(u -> u.getAge() > 18)
map (Stream)
Function<T, R>
map(u -> u.getName())
forEach (Stream, List)
Consumer<T>
forEach(u -> System.out.println(u))
generate (Stream)
Supplier<T>
Stream.generate(() -> ...)

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.

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION