CodeGym /Corsi /JAVA 25 SELF /Operazioni di base della Stream API: map, filter, collect...

Operazioni di base della Stream API: map, filter, collect

JAVA 25 SELF
Livello 30 , Lezione 1
Disponibile

1. Creiamo uno stream

Per usare la Stream API, devi prima ottenere uno stream da una collezione o da un array.

Esempi di creazione di uno stream

// Da una lista
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();

// Da un array
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);

// Da valori singoli
Stream<String> letters = Stream.of("A", "B", "C");

In breve:

  • list.stream() — per le collezioni
  • Arrays.stream(array) — per gli array
  • Stream.of(...) — per valori singoli

Esempio nel contesto della nostra applicazione

Supponiamo di avere una lista di utenti:

List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();

Operazioni intermedie e terminali

Punto importante: le operazioni nella Stream API si dividono in due tipi.

  • Operazioni intermedie (ad esempio, filter, map, distinct) — descrivono le fasi di elaborazione. Restituiscono un nuovo stream, ma di per sé non avviano nulla.
  • Operazioni terminali (ad esempio, collect, forEach, count) — avviano la pipeline e producono il risultato.

Lo stream è «pigro»: finché non viene chiamata un’operazione terminale, non avviene alcun calcolo. Ecco perché spesso concludiamo la catena con collect(...): è il punto in cui lo stream si trasforma di nuovo in una collezione o in un altro risultato.

2. Operazione filter: filtrare gli elementi per condizione

filter è un’operazione intermedia che lascia passare solo gli elementi che soddisfano una data condizione.

Firma

Stream<T> filter(Predicate<? super T> predicate); 

Predicate è un’interfaccia funzionale che prende un elemento e restituisce true (tenere) oppure false (scartare).

Esempio: tenere solo i numeri pari

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evenNumbers = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());

System.out.println(evenNumbers); // [2, 4, 6]

Cosa succede?

  • n -> n % 2 == 0 — espressione lambda che verifica se il numero è divisibile per 2 senza resto.
  • filter lascia solo i numeri pari.

Esempio: filtrare i nomi che iniziano con «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<String> aNames = names.stream()
    .filter(name -> name.startsWith("A"))
    .collect(Collectors.toList());

System.out.println(aNames); // [Anna, Alex, Alina]

Punto importante: filter non modifica la collezione: crea un nuovo stream che contiene solo gli elementi necessari.

3. Operazione map: trasformare un elemento in qualcos’altro

map è un’operazione di trasformazione. Prende ogni elemento dello stream, applica una funzione e restituisce un nuovo elemento.

Firma

<R> Stream<R> map(Function<? super T, ? extends R> mapper)

Function è un’interfaccia che prende un elemento e restituisce qualcosa (anche di un altro tipo).

Esempio: ottenere le lunghezze delle stringhe

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

List<Integer> nameLengths = names.stream()
    .map(name -> name.length())
    .collect(Collectors.toList());

System.out.println(nameLengths); // [4, 5, 4]

Cosa succede?

  • map trasforma la stringa nella sua lunghezza (name -> name.length()).
  • Il risultato è uno stream di numeri.

Esempio: portare le stringhe in maiuscolo

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

List<String> upperNames = names.stream()
    .map(name -> name.toUpperCase())
    .collect(Collectors.toList());

System.out.println(upperNames); // [ANNA, BORIS, ALEX]

4. Operazione collect: raccogliere il risultato di nuovo in una collezione

collect è un’operazione terminale, cioè conclude lo stream e raccoglie il risultato in una collezione o in un altro contenitore.

Firma

<R, A> R collect(Collector<? super T, A, R> collector)

Niente paura per la firma spaventosa! Nel 99% dei casi userete i collector già pronti della classe Collectors.

Collectors è una classe di utilità con un insieme di «raccoglitori». Indica allo stream in quale forma raccogliere il risultato: lista, insieme, stringa, ecc.

Esempi:

  • Collectors.toList() — in List
  • Collectors.toSet() — in Set
  • Collectors.joining(", ") — in una stringa separata da virgola

In altre parole, Collectors è come un set di scatole di forme diverse in cui impacchettate gli elementi dello stream.

Esempio: raccogliere il risultato in una List

List<String> filtered = names.stream()
    .filter(name -> name.length() > 3)
    .collect(Collectors.toList());

Esempio: raccogliere il risultato in un Set

Set<String> uniqueNames = names.stream()
    .map(String::toLowerCase)
    .collect(Collectors.toSet());

Esempio: unire le stringhe con una virgola

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result); // Anna, Boris, Alex

5. Catena di operazioni: filtro + trasformazione + raccolta del risultato

Il punto di forza principale della Stream API è la possibilità di concatenare le operazioni l’una dopo l’altra.

Esempio: ottenere le lunghezze dei nomi che iniziano con «A»

List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");

List<Integer> aNameLengths = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::length)
    .collect(Collectors.toList());

System.out.println(aNameLengths); // [4, 4, 5]

Passo per passo:

  1. .stream() — creiamo uno stream dalla lista.
  2. .filter(name -> name.startsWith("A")) — lasciamo solo i nomi che iniziano per "A".
  3. .map(String::length) — trasformiamo ogni nome nella sua lunghezza.
  4. .collect(Collectors.toList()) — raccogliamo il risultato in una lista.

Codice imperativo equivalente

Ecco come apparirebbe lo stesso «alla vecchia maniera»:

List<Integer> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.length());
    }
}

Confronta: Stream API — una riga, si legge come «cosa facciamo», non «come lo facciamo».

6. Pratica: alcuni brevi esercizi

Alleniamoci! Tutti gli esempi si possono eseguire in un unico file — basta cambiare i dati.

Esercizio 1: lasciare solo i numeri dispari e elevarli al quadrato

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);

List<Integer> oddSquares = numbers.stream()
    .filter(n -> n % 2 != 0)
    .map(n -> n * n)
    .collect(Collectors.toList());

System.out.println(oddSquares); // [1, 9, 25, 49]

Esercizio 2: da una lista di stringhe ottenere la lista delle loro prime lettere

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

List<Character> initials = names.stream()
    .map(name -> name.charAt(0))
    .collect(Collectors.toList());

System.out.println(initials); // [A, B, A]

Esercizio 3: filtrare le stringhe con lunghezza > 3 e raccoglierle in un Set

List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");

Set<String> longWords = words.stream()
    .filter(word -> word.length() > 3)
    .collect(Collectors.toSet());

System.out.println(longWords); // [bear, elephant]

7. Errori tipici lavorando con filter, map, collect

Errore n. 1: hai dimenticato collect — nessun risultato!
La Stream API è pigra come un gatto sul davanzale: finché non chiami un’operazione terminale (ad esempio, collect o forEach), non succederà nulla. Se scrivi solo users.stream().filter(...).map(...); — non verrà eseguita alcuna azione.

Errore n. 2: filter e map scambiati di posto
A volte i principianti applicano prima map e poi filter. Per esempio, names.stream().map(String::length).filter(len -> len > 3) — produce numeri, non stringhe. Se ti servono stringhe di lunghezza maggiore di 3, prima filtra e poi trasforma.

Errore n. 3: dimenticare l’immutabilità
Le operazioni della Stream API non modificano la collezione originale! Restituiscono un nuovo risultato. Dopo List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); — la collezione names resterà invariata.

Errore n. 4: tentare di usare una lista esterna mutabile
Meglio evitare così:

List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));

Meglio usare collect: è più sicuro e più conciso.

Errore n. 5: NullPointerException
Se nella collezione possono esserci elementi null, chiamare name.startsWith("A") su null causerà un errore. Aggiungi un filtro per null quando possibile:

.filter(name -> name != null && name.startsWith("A"))
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION