Salta al contenuto principale

10 - Programmazione Funzionale e Stream API

Programmazione funzionale in Java: lambda expressions, interfacce funzionali, method references, Stream API, Collectors e Optional.

Cosa ne pensi di questo articolo?

10 - Programmazione Funzionale e Stream API
JavaFoto:Bibek ghoshPexels

In Breve

Programmazione funzionale in Java: lambda expressions, interfacce funzionali, method references, Stream API, Collectors e Optional.

Programmazione Funzionale in Java

A partire da Java 8, il linguaggio supporta la programmazione funzionale attraverso lambda expressions, method references, la Stream API e Optional. Questi strumenti permettono codice più conciso, leggibile e dichiarativo.

Cosa Imparerai

  • Lambda expressions e sintassi
  • Interfacce funzionali
  • Method references
  • Stream API: operazioni intermedie e terminali
  • Collectors e raggruppamento
  • Optional per gestire null

Lambda Expressions

Una lambda è una funzione anonima che può essere passata come parametro o assegnata a una variabile.

Sintassi Lambda
// Prima di Java 8 - classe anonima
Comparator<String> comparatorVecchio = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

// Con Lambda - Java 8+
Comparator<String> comparatorLambda = (s1, s2) -> s1.length() - s2.length();

// Varianti sintattiche
// Un solo parametro: parentesi opzionali
Consumer<String> stampa = s -> System.out.println(s);

// Nessun parametro: parentesi vuote obbligatorie
Runnable azione = () -> System.out.println("Eseguito!");

// Corpo con più istruzioni: usare blocco {}
Comparator<String> conBlocco = (s1, s2) -> {
    int diff = s1.length() - s2.length();
    return diff != 0 ? diff : s1.compareTo(s2);
};

Interfacce Funzionali

Interfacce Funzionali Principali

Interfaccia Metodo Uso
Predicate<T> boolean test(T) Condizioni, filtri
Function<T,R> R apply(T) Trasformazioni
Consumer<T> void accept(T) Azioni su elementi
Supplier<T> T get() Generazione valori
BiFunction<T,U,R> R apply(T,U) Due input, un output
UnaryOperator<T> T apply(T) Stesso tipo in/out
Esempi Interfacce Funzionali
import java.util.function.*;

public class EsempiFunzionali {
    public static void main(String[] args) {
        // Predicate: condizione
        Predicate<Integer> isSufficiente = voto -> voto >= 18;
        System.out.println(isSufficiente.test(25));  // true
        System.out.println(isSufficiente.test(15));  // false

        // Function: trasformazione
        Function<String, Integer> lunghezza = s -> s.length();
        System.out.println(lunghezza.apply("Java"));  // 4

        // Consumer: azione
        Consumer<String> saluta = nome -> System.out.println("Ciao " + nome);
        saluta.accept("Mario");  // Ciao Mario

        // Supplier: generazione
        Supplier<Double> randomVoto = () -> 18 + Math.random() * 13;
        System.out.println(randomVoto.get());  // voto casuale 18-30

        // Composizione
        Predicate<Integer> isEccellente = voto -> voto >= 28;
        Predicate<Integer> isBuono = isSufficiente.and(isEccellente.negate());

        Function<String, String> maiuscolo = String::toUpperCase;
        Function<String, String> conPrefisso = s -> "Prof. " + s;
        Function<String, String> titolo = maiuscolo.andThen(conPrefisso);
        System.out.println(titolo.apply("rossi"));  // Prof. ROSSI
    }
}

Method References

Tipi di Method Reference
import java.util.*;

public class MethodReferences {
    public static void main(String[] args) {
        List<String> nomi = Arrays.asList("Mario", "Laura", "Giuseppe");

        // 1. Riferimento a metodo statico
        // Lambda:  s -> System.out.println(s)
        nomi.forEach(System.out::println);

        // 2. Riferimento a metodo di istanza (oggetto specifico)
        String prefisso = "Studente: ";
        // Lambda: s -> prefisso.concat(s)
        nomi.stream()
            .map(prefisso::concat)
            .forEach(System.out::println);

        // 3. Riferimento a metodo di istanza (tipo)
        // Lambda: s -> s.toUpperCase()
        nomi.stream()
            .map(String::toUpperCase)
            .forEach(System.out::println);

        // 4. Riferimento a costruttore
        // Lambda: s -> new StringBuilder(s)
        nomi.stream()
            .map(StringBuilder::new)
            .forEach(sb -> System.out.println(sb.reverse()));
    }
}

Stream API

Gli Stream permettono di elaborare sequenze di elementi in modo dichiarativo, con supporto per operazioni parallele.

Creare Stream
import java.util.*;
import java.util.stream.*;

public class CreazioneStream {
    public static void main(String[] args) {
        // Da Collection
        List<String> lista = Arrays.asList("A", "B", "C");
        Stream<String> streamLista = lista.stream();

        // Da array
        String[] array = {"A", "B", "C"};
        Stream<String> streamArray = Arrays.stream(array);

        // Stream.of()
        Stream<String> streamOf = Stream.of("A", "B", "C");

        // Stream.generate() - infinito
        Stream<Double> casuali = Stream.generate(Math::random).limit(5);

        // Stream.iterate() - sequenza
        Stream<Integer> pari = Stream.iterate(0, n -> n + 2).limit(10);

        // IntStream, LongStream, DoubleStream - primitivi
        IntStream interi = IntStream.range(1, 11);  // 1-10
        IntStream interiChiuso = IntStream.rangeClosed(1, 10);  // 1-10

        // Da file (righe)
        // Stream<String> righe = Files.lines(Path.of("file.txt"));
    }
}

Operazioni Intermedie

Filter, Map, FlatMap
import java.util.*;
import java.util.stream.*;

public class OperazioniIntermedie {
    public static void main(String[] args) {
        List<Studente> studenti = Arrays.asList(
            new Studente("Mario", 28, "Informatica"),
            new Studente("Laura", 30, "Matematica"),
            new Studente("Giuseppe", 25, "Informatica"),
            new Studente("Anna", 27, "Fisica")
        );

        // FILTER: seleziona elementi
        List<Studente> promossi = studenti.stream()
            .filter(s -> s.getVoto() >= 18)
            .collect(Collectors.toList());

        // MAP: trasforma elementi
        List<String> nomi = studenti.stream()
            .map(Studente::getNome)
            .collect(Collectors.toList());

        // MAP con trasformazione
        List<String> nomiMaiuscoli = studenti.stream()
            .map(s -> s.getNome().toUpperCase())
            .collect(Collectors.toList());

        // FLATMAP: appiattisce stream di stream
        List<List<String>> corsiPerStudente = Arrays.asList(
            Arrays.asList("Java", "Python"),
            Arrays.asList("SQL", "MongoDB"),
            Arrays.asList("Java", "JavaScript")
        );

        List<String> tuttiCorsi = corsiPerStudente.stream()
            .flatMap(List::stream)
            .distinct()
            .collect(Collectors.toList());
        // [Java, Python, SQL, MongoDB, JavaScript]

        // SORTED: ordina
        List<Studente> perVoto = studenti.stream()
            .sorted(Comparator.comparing(Studente::getVoto).reversed())
            .collect(Collectors.toList());

        // DISTINCT: rimuove duplicati
        List<String> corsiUnici = studenti.stream()
            .map(Studente::getCorso)
            .distinct()
            .collect(Collectors.toList());

        // PEEK: debug (non modifica lo stream)
        studenti.stream()
            .peek(s -> System.out.println("Elaborando: " + s.getNome()))
            .filter(s -> s.getVoto() >= 28)
            .forEach(System.out::println);

        // LIMIT e SKIP
        List<Studente> primi2 = studenti.stream()
            .limit(2)
            .collect(Collectors.toList());

        List<Studente> dopoPrimi2 = studenti.stream()
            .skip(2)
            .collect(Collectors.toList());
    }
}

class Studente {
    private String nome;
    private int voto;
    private String corso;

    public Studente(String nome, int voto, String corso) {
        this.nome = nome;
        this.voto = voto;
        this.corso = corso;
    }

    public String getNome() { return nome; }
    public int getVoto() { return voto; }
    public String getCorso() { return corso; }
}

Operazioni Terminali

Collect, Reduce, Match, Find
import java.util.*;
import java.util.stream.*;

public class OperazioniTerminali {
    public static void main(String[] args) {
        List<Integer> voti = Arrays.asList(28, 30, 25, 27, 30, 24);

        // FOREACH: azione su ogni elemento
        voti.stream().forEach(System.out::println);

        // COUNT: conta elementi
        long quanti = voti.stream()
            .filter(v -> v >= 28)
            .count();  // 3

        // SUM, AVERAGE, MIN, MAX (IntStream)
        int somma = voti.stream()
            .mapToInt(Integer::intValue)
            .sum();

        OptionalDouble media = voti.stream()
            .mapToInt(Integer::intValue)
            .average();

        OptionalInt massimo = voti.stream()
            .mapToInt(Integer::intValue)
            .max();

        // REDUCE: riduce a singolo valore
        int sommaReduce = voti.stream()
            .reduce(0, (a, b) -> a + b);

        Optional<Integer> maxReduce = voti.stream()
            .reduce(Integer::max);

        // MATCH: verifica condizioni
        boolean tuttiSufficienti = voti.stream()
            .allMatch(v -> v >= 18);  // true

        boolean almenoUn30 = voti.stream()
            .anyMatch(v -> v == 30);  // true

        boolean nessunoInsuff = voti.stream()
            .noneMatch(v -> v < 18);  // true

        // FIND: trova elementi
        Optional<Integer> primo = voti.stream()
            .filter(v -> v >= 28)
            .findFirst();

        Optional<Integer> qualsiasi = voti.stream()
            .filter(v -> v >= 28)
            .findAny();  // Utile con parallelStream

        // TOARRAY
        Integer[] arrayVoti = voti.stream()
            .filter(v -> v >= 25)
            .toArray(Integer[]::new);
    }
}

Collectors Avanzati

Raggruppamento e Partizionamento
import java.util.*;
import java.util.stream.*;

public class CollectorsAvanzati {
    public static void main(String[] args) {
        List<Studente> studenti = Arrays.asList(
            new Studente("Mario", 28, "Informatica"),
            new Studente("Laura", 30, "Matematica"),
            new Studente("Giuseppe", 25, "Informatica"),
            new Studente("Anna", 27, "Fisica"),
            new Studente("Marco", 30, "Informatica")
        );

        // GROUPING BY: raggruppa per chiave
        Map<String, List<Studente>> perCorso = studenti.stream()
            .collect(Collectors.groupingBy(Studente::getCorso));
        // {Informatica=[Mario, Giuseppe, Marco], Matematica=[Laura], Fisica=[Anna]}

        // GroupingBy con downstream collector
        Map<String, Long> contaPerCorso = studenti.stream()
            .collect(Collectors.groupingBy(
                Studente::getCorso,
                Collectors.counting()
            ));
        // {Informatica=3, Matematica=1, Fisica=1}

        Map<String, Double> mediaPerCorso = studenti.stream()
            .collect(Collectors.groupingBy(
                Studente::getCorso,
                Collectors.averagingInt(Studente::getVoto)
            ));

        Map<String, Optional<Studente>> migliorPerCorso = studenti.stream()
            .collect(Collectors.groupingBy(
                Studente::getCorso,
                Collectors.maxBy(Comparator.comparing(Studente::getVoto))
            ));

        // PARTITIONING BY: divide in true/false
        Map<Boolean, List<Studente>> promossiVsNon = studenti.stream()
            .collect(Collectors.partitioningBy(s -> s.getVoto() >= 18));
        // {true=[tutti], false=[]}

        Map<Boolean, Long> contaPromossi = studenti.stream()
            .collect(Collectors.partitioningBy(
                s -> s.getVoto() >= 28,
                Collectors.counting()
            ));

        // JOINING: concatena stringhe
        String nomiConcatenati = studenti.stream()
            .map(Studente::getNome)
            .collect(Collectors.joining(", "));
        // "Mario, Laura, Giuseppe, Anna, Marco"

        String conPrefissoSuffisso = studenti.stream()
            .map(Studente::getNome)
            .collect(Collectors.joining(", ", "Studenti: [", "]"));
        // "Studenti: [Mario, Laura, Giuseppe, Anna, Marco]"

        // TO MAP
        Map<String, Integer> nomeVoto = studenti.stream()
            .collect(Collectors.toMap(
                Studente::getNome,
                Studente::getVoto
            ));

        // SUMMARIZING
        IntSummaryStatistics stats = studenti.stream()
            .collect(Collectors.summarizingInt(Studente::getVoto));
        System.out.println("Min: " + stats.getMin());
        System.out.println("Max: " + stats.getMax());
        System.out.println("Media: " + stats.getAverage());
        System.out.println("Somma: " + stats.getSum());
        System.out.println("Count: " + stats.getCount());
    }
}

Optional

Optional è un container che può contenere o meno un valore, evitando NullPointerException.

Uso di Optional
import java.util.*;

public class EsempiOptional {

    public static Optional<Studente> trovaStudente(List<Studente> studenti,
                                                    String nome) {
        return studenti.stream()
            .filter(s -> s.getNome().equals(nome))
            .findFirst();
    }

    public static void main(String[] args) {
        List<Studente> studenti = Arrays.asList(
            new Studente("Mario", 28, "Informatica"),
            new Studente("Laura", 30, "Matematica")
        );

        // Creare Optional
        Optional<String> pieno = Optional.of("Valore");
        Optional<String> vuoto = Optional.empty();
        Optional<String> nullable = Optional.ofNullable(null);

        // isPresent e isEmpty
        Optional<Studente> mario = trovaStudente(studenti, "Mario");
        if (mario.isPresent()) {
            System.out.println("Trovato: " + mario.get().getVoto());
        }

        // ifPresent: esegue azione se presente
        mario.ifPresent(s -> System.out.println("Voto: " + s.getVoto()));

        // ifPresentOrElse (Java 9+)
        Optional<Studente> paolo = trovaStudente(studenti, "Paolo");
        paolo.ifPresentOrElse(
            s -> System.out.println("Trovato: " + s.getNome()),
            () -> System.out.println("Studente non trovato")
        );

        // orElse: valore default
        Studente result = paolo.orElse(new Studente("Default", 0, "N/A"));

        // orElseGet: supplier per default (lazy)
        Studente resultLazy = paolo.orElseGet(
            () -> new Studente("Default", 0, "N/A")
        );

        // orElseThrow: eccezione se vuoto
        Studente obbligatorio = mario.orElseThrow(
            () -> new NoSuchElementException("Studente richiesto!")
        );

        // map: trasforma se presente
        Optional<String> nomeStudente = mario.map(Studente::getNome);

        // flatMap: per Optional nested
        Optional<String> corsoOpt = mario.flatMap(s ->
            Optional.ofNullable(s.getCorso())
        );

        // filter: filtra valore
        Optional<Studente> eccellente = mario
            .filter(s -> s.getVoto() >= 28);

        // Chaining completo
        String risultato = trovaStudente(studenti, "Mario")
            .filter(s -> s.getVoto() >= 25)
            .map(Studente::getNome)
            .map(String::toUpperCase)
            .orElse("NON TROVATO");
        System.out.println(risultato);  // MARIO
    }
}

Esempio Completo: Analisi Registro Studenti

AnalisiRegistro.java
import java.util.*;
import java.util.stream.*;

public class AnalisiRegistro {

    public static void main(String[] args) {
        List<Esame> esami = Arrays.asList(
            new Esame("Mario", "Programmazione", 28, 2024),
            new Esame("Mario", "Database", 30, 2024),
            new Esame("Mario", "Reti", 25, 2024),
            new Esame("Laura", "Programmazione", 30, 2024),
            new Esame("Laura", "Database", 29, 2024),
            new Esame("Giuseppe", "Programmazione", 24, 2024),
            new Esame("Giuseppe", "Database", 26, 2023),
            new Esame("Anna", "Programmazione", 30, 2024),
            new Esame("Anna", "Reti", 28, 2024)
        );

        // 1. Media voti per studente
        System.out.println("=== Media per studente ===");
        Map<String, Double> mediaPerStudente = esami.stream()
            .collect(Collectors.groupingBy(
                Esame::getStudente,
                Collectors.averagingInt(Esame::getVoto)
            ));
        mediaPerStudente.forEach((nome, media) ->
            System.out.printf("%s: %.2f%n", nome, media)
        );

        // 2. Studente con media più alta
        System.out.println("\n=== Migliore studente ===");
        mediaPerStudente.entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .ifPresent(e ->
                System.out.printf("%s con media %.2f%n", e.getKey(), e.getValue())
            );

        // 3. Distribuzione voti
        System.out.println("\n=== Distribuzione voti ===");
        Map<String, Long> distribuzione = esami.stream()
            .collect(Collectors.groupingBy(
                e -> {
                    int v = e.getVoto();
                    if (v >= 28) return "Ottimo (28-30)";
                    if (v >= 24) return "Buono (24-27)";
                    return "Sufficiente (18-23)";
                },
                Collectors.counting()
            ));
        distribuzione.forEach((fascia, count) ->
            System.out.println(fascia + ": " + count)
        );

        // 4. Esami per materia con lista studenti
        System.out.println("\n=== Studenti per materia ===");
        Map<String, String> studentiPerMateria = esami.stream()
            .collect(Collectors.groupingBy(
                Esame::getMateria,
                Collectors.mapping(
                    Esame::getStudente,
                    Collectors.joining(", ")
                )
            ));
        studentiPerMateria.forEach((materia, studenti) ->
            System.out.println(materia + ": " + studenti)
        );

        // 5. Top 3 voti
        System.out.println("\n=== Top 3 voti ===");
        esami.stream()
            .sorted(Comparator.comparing(Esame::getVoto).reversed())
            .limit(3)
            .forEach(e -> System.out.printf("%s - %s: %d%n",
                e.getStudente(), e.getMateria(), e.getVoto()));

        // 6. Studenti con tutti esami >= 28
        System.out.println("\n=== Studenti eccellenti ===");
        Map<String, Boolean> tuttiEccellenti = esami.stream()
            .collect(Collectors.groupingBy(
                Esame::getStudente,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    list -> list.stream().allMatch(e -> e.getVoto() >= 28)
                )
            ));
        tuttiEccellenti.entrySet().stream()
            .filter(Map.Entry::getValue)
            .map(Map.Entry::getKey)
            .forEach(System.out::println);

        // 7. Statistiche complete
        System.out.println("\n=== Statistiche ===");
        IntSummaryStatistics stats = esami.stream()
            .mapToInt(Esame::getVoto)
            .summaryStatistics();
        System.out.println("Totale esami: " + stats.getCount());
        System.out.println("Media: " + String.format("%.2f", stats.getAverage()));
        System.out.println("Min: " + stats.getMin());
        System.out.println("Max: " + stats.getMax());
    }
}

class Esame {
    private String studente;
    private String materia;
    private int voto;
    private int anno;

    public Esame(String studente, String materia, int voto, int anno) {
        this.studente = studente;
        this.materia = materia;
        this.voto = voto;
        this.anno = anno;
    }

    public String getStudente() { return studente; }
    public String getMateria() { return materia; }
    public int getVoto() { return voto; }
    public int getAnno() { return anno; }
}

Conclusione

La programmazione funzionale in Java rende il codice più conciso e leggibile. Lambda, Stream e Optional sono strumenti essenziali per lo sviluppo moderno.

Punti Chiave da Ricordare

  • Lambda: Funzioni anonime per codice conciso
  • Method Reference: Sintassi abbreviata (Class::method)
  • Stream: Pipeline dichiarative per elaborazione dati
  • Collectors: Raggruppamento, partizionamento, statistiche
  • Optional: Gestione sicura di valori nullable
  • Lazy evaluation: Gli stream non elaborano finché non serve

Nel prossimo articolo affronteremo concorrenza e multithreading: Thread, ExecutorService e sincronizzazione.

Domande frequenti

Cos'è la programmazione funzionale in Java?

La programmazione funzionale in Java è un insieme di strumenti introdotti a partire da Java 8 che permettono di scrivere codice più conciso, leggibile e dichiarativo.

Quali sono gli strumenti principali della programmazione funzionale in Java?

I principali strumenti della programmazione funzionale in Java sono lambda expressions, method references, la Stream API e Optional.

Cos'è una lambda expression in Java?

Una lambda è una funzione anonima che può essere passata come parametro o assegnata a una variabile.

Qual è lo scopo dell'Optional in Java?

L'Optional è un container che può contenere o meno un valore, evitando di dover gestire eventuali eccezioni.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti