CodeGym /Corsi /JAVA 25 SELF /Passare funzioni come parametri: esempi

Passare funzioni come parametri: esempi

JAVA 25 SELF
Livello 49 , Lezione 2
Disponibile

1. Passare il comportamento invece dei dati

Perché mai passare una funzione come parametro?

Immaginate: avete un metodo che ordina una lista. Ma come fa a sapere in che modo ordinarla? Per ordine alfabetico? Per lunghezza? Per data di nascita? Si potrebbe scrivere un metodo separato per ogni caso, ma questo si trasformerebbe presto in un inferno di copia-incolla.

Invece, Java consente di passare il comportamento (cioè una funzione o una lambda) che definisce come ordinare, filtrare, trasformare e così via. Questo rende il codice flessibile e riutilizzabile.

Esempio: ordinamento con un comparator

List<String> names = List.of("Anna", "Boris", "Vika");

// Passiamo il comportamento: come confrontare gli elementi (per lunghezza della stringa)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

Esempio: filtraggio con un predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Passiamo il comportamento: chi tenere (nome più lungo di 4 caratteri)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

In entrambi i casi non avete “hardcodato” la logica dentro il metodo, ma gli avete passato un pezzetto di comportamento che viene applicato a ogni elemento.

Vantaggi: meno duplicazione, più flessibilità

Invece di decine di metodi con codice simile ma logiche diverse, scrivete un unico metodo generico che accetta “che cosa fare” sotto forma di funzione. Questo fa risparmiare tempo, riduce gli errori e rende il codice più semplice da testare.

2. Sintassi del passaggio di funzioni

Espressioni lambda come argomenti

Il modo più comune — scrivere una lambda con la freccia -> direttamente nel punto di chiamata del metodo:

list.forEach(item -> System.out.println(item));

Oppure ancora più corto, se esiste già un metodo adatto — un riferimento al metodo (::):

list.forEach(System.out::println);

Riferimenti a metodi (method references)

Se avete già un metodo adatto, potete passarlo come riferimento :::

// Metodo normale
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// Passare un riferimento al metodo
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

Funziona se la firma del metodo coincide con quella attesa dall’interfaccia funzionale (Predicate<T>, Function<T, R>, Comparator<T> ecc.).

3. Esempi dalla libreria standard

Collections.sort e Comparator

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));

// Ordinamento per lunghezza del nome
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]

Stream.filter e Predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// Lasciare solo i nomi che iniziano con 'V'
names.stream()
     .filter(name -> name.startsWith("V"))
     .forEach(System.out::println); // Vika

Stream.map e Function

List<String> names = List.of("Anna", "Boris", "Vika");

// Trasformare i nomi in maiuscolo
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent e Consumer

Optional<String> opt = Optional.of("Ciao!");

// Se il valore è presente, stamparlo
opt.ifPresent(s -> System.out.println("Nella stringa: " + s));

4. Pratica: scrivere metodi propri con funzioni parametro

È ora di mettere in pratica! Scriviamo alcuni metodi che accettano una funzione come parametro e la usano al loro interno.

Esempio 1: Metodo per elaborare gli elementi di una lista (forEach a modo nostro)

Supponiamo di avere una lista di utenti (User). Vogliamo eseguire su ogni utente qualche azione — per esempio, stampare il nome, inviare un’e‑mail, accreditare bonus, e così via. Invece di “hardcodare” l’azione, la passeremo come parametro — un Consumer<User>!

import java.util.List;
import java.util.function.Consumer;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class UserProcessor {
    // Metodo che accetta Consumer<User>
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 30),
            new User("Vika", 22)
        );

        // Stampare i nomi di tutti gli utenti
        processUsers(users, user -> System.out.println("Nome: " + user.name));

        // Accreditare un bonus (per esempio, solo stampare)
        processUsers(users, user -> System.out.println(user.name + " ha ricevuto un bonus!"));
    }
}

Esempio 2: Metodo per filtrare elementi (Predicate)

Scriviamo un metodo che restituisce solo gli utenti che soddisfano una condizione (per esempio, solo i maggiorenni):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // Filtriamo solo i maggiorenni
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
    }
}

Esempio 3: Metodo di trasformazione (Function)

Ora un metodo che da una lista di utenti ricava la lista dei loro nomi (o qualsiasi altra trasformazione):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // Otteniamo l'elenco dei nomi
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Anna, Boris, Vika]

        // Otteniamo l'elenco delle età
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

Esempio 4: Metodo generatore (Supplier)

A volte serve ottenere un certo valore “on demand” — ad esempio generare un numero casuale, creare un oggetto, ottenere l’ora corrente. A questo scopo è adatta l’interfaccia Supplier<T>.

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // Otteniamo un numero casuale
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("Numero casuale: " + random);

        // Otteniamo l'ora corrente in millisecondi
        long time = getValue(System::currentTimeMillis);
        System.out.println("Tempo: " + time);
    }
}

5. Applicazione unica: mettiamo insieme gli esempi

Immaginiamo di sviluppare una semplice applicazione “Elenco utenti”. Sappiamo già:

  • Filtrare gli utenti in base a una condizione (ad esempio, solo i maggiorenni);
  • Trasformare gli utenti in qualcos’altro (nomi, e‑mail, età);
  • Eseguire azioni su ogni utente (stampa, accredito di bonus);
  • Generare valori su richiesta (per esempio, creare nuovi utenti).

Ora, usando tutti questi metodi, possiamo costruire scenari flessibili di elaborazione dei dati, senza riscrivere il codice ogni volta per un nuovo compito.

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // Metodo universale per elaborare gli utenti
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // Filtro universale
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // Trasformatore universale
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 1. Stampare tutti gli utenti
        processUsers(users, user -> System.out.println("Utente: " + user));

        // 2. Trovare solo gli adulti
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("Maggiorenni: " + adults);

        // 3. Ottenere i nomi degli adulti
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("Nomi degli adulti: " + adultNames);

        // 4. Accreditare un bonus agli adulti
        processUsers(adults, user -> System.out.println(user.name + " ha ricevuto un bonus!"));
    }
}

Perché questo è meglio dell’approccio “classico”?

Questo approccio offre flessibilità: gli stessi metodi possono essere usati in scenari completamente diversi semplicemente passando funzioni diverse. Il codice non esplode in infiniti varianti “filtra così”, “stampa cosà” — abbiamo strumenti generali che funzionano ovunque. Grazie a ciò il codice risulta più compatto, più chiaro e meno incline agli errori. Inoltre questo stile si integra perfettamente con il moderno Stream API, quindi passando ai flussi non dovrete reimparare nulla: l’approccio è lo stesso.

6. Errori tipici nel passare funzioni come parametri

Errore n. 1: La firma non coincide con l’interfaccia attesa.
Se un metodo accetta Predicate<User>, ma passate una lambda che restituisce non boolean bensì, per esempio, una String, il compilatore darà subito errore e non vi lascerà compilare il progetto. Controllate che il valore restituito e i parametri coincidano con quelli attesi.

Errore n. 2: La lambda usa variabili che possono cambiare.
In Java le espressioni lambda possono usare solo variabili final o effectively final dal contesto esterno. Se provate a modificare una di queste variabili dentro la lambda, otterrete un errore di compilazione.

Errore n. 3: Confusione tra interfacce.
A volte si vuole passare, per esempio, un Consumer<T>, ma per errore si scrive una funzione che restituisce qualcosa (per esempio, una Function<T, R>). Verificate che la vostra lambda restituisca esattamente ciò che serve (o nulla — per il Consumer).

Errore n. 4: Lambda troppo complesse direttamente nei parametri.
Se una lambda occupa più di una-due righe, è meglio estrarla in una variabile o in un metodo separato. Altrimenti il codice diventerà illeggibile, e a capirlo sarete solo voi (e nemmeno sempre).

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