Stream-uri (ideea) în Java

Generics, lambda și stream-uri Java 8 (toList(): Java 16)

Un stream descrie o prelucrare de date ca un lanț: sursă, operații intermediare (filter, map, sorted) și o operație finală (toList, count, collect).

Sintaxă
rezultat = sursa.stream()        // 1. sursa
    .filter(x -> ...)            // 2. operații intermediare (leneșe)
    .map(x -> ...)
    .sorted()
    .toList();                   // 3. operație finală (pornește calculul)

Ce face Stream-uri (ideea)

Un stream nu este o colecție, ci o „bandă rulantă” prin care trec elementele unei surse (o listă, un array, un fișier, un interval de numere). Scrii ce vrei (păstrează elevii promovați, ia-le numele, sortează-le) în loc de cum (bucle, variabile temporare, if-uri).

Operațiile intermediare (filter, map, sorted, distinct, limit) returnează un stream nou și sunt leneșe: nu se execută nimic până la operația finală (toList, count, sum, forEach, collect, anyMatch). Sursa nu este modificată.

Un stream se poate parcurge o singură dată. Pentru numere primitive există IntStream, LongStream și DoubleStream, cu sum(), average() și range(). Detaliile fiecărei operații sunt pe paginile Stream și Collectors.

Exemple

Exemplul 1: Buclă clasică și stream

Ambele variante calculează numele elevilor promovați, sortate. Varianta cu stream citește ca o descriere a pașilor.

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

record Elev(String nume, double nota) { }

public class Main {
    public static void main(String[] args) {
        List<Elev> elevi = List.of(
            new Elev("Radu", 4.5), new Elev("Ana", 9.0), new Elev("Victor", 7.25),
            new Elev("Bianca", 3.0), new Elev("Dan", 6.0));

        // cu buclă
        List<String> promovati = new ArrayList<>();
        for (Elev e : elevi) {
            if (e.nota() >= 5) {
                promovati.add(e.nume().toUpperCase());
            }
        }
        Collections.sort(promovati);
        System.out.println("buclă:  " + promovati);

        // cu stream
        List<String> cuStream = elevi.stream()
            .filter(e -> e.nota() >= 5)
            .map(e -> e.nume().toUpperCase())
            .sorted()
            .toList();
        System.out.println("stream: " + cuStream);

        double medie = elevi.stream().mapToDouble(Elev::nota).average().orElse(0);
        System.out.println("media clasei: " + medie);
    }
}

Rezultat

buclă:  [ANA, DAN, VICTOR]
stream: [ANA, DAN, VICTOR]
media clasei: 5.95

Exemplul 2: Evaluare leneșă: nimic nu rulează fără operația finală

Primul lanț nu are operație finală, deci peek nu afișează nimic. În al doilea, findFirst se oprește la primul număr potrivit, deci elementele de după nu mai sunt prelucrate.

import java.util.List;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        List<Integer> numere = List.of(3, 8, 12, 5, 20);

        Stream<Integer> neterminat = numere.stream().peek(n -> System.out.println("văd " + n));
        System.out.println("stream creat, dar nimic afișat încă");

        int primulMare = numere.stream()
            .peek(n -> System.out.println("verific " + n))
            .filter(n -> n > 10)
            .findFirst()
            .orElse(-1);
        System.out.println("primul > 10: " + primulMare);

        neterminat.close();
    }
}

Rezultat

stream creat, dar nimic afișat încă
verific 3
verific 8
verific 12
primul > 10: 12

Exemplul 3: Grupare și numărare

Collectors.groupingBy grupează comenzile pe oraș; TreeMap::new ține orașele sortate. IntStream.rangeClosed produce numerele 1..5.

import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

record Comanda(String oras, double valoare) { }

public class Main {
    public static void main(String[] args) {
        List<Comanda> comenzi = List.of(
            new Comanda("Iași", 120), new Comanda("Cluj", 80), new Comanda("Iași", 45.5),
            new Comanda("Brașov", 200), new Comanda("Cluj", 60));

        Map<String, Double> totalPeOras = comenzi.stream()
            .collect(Collectors.groupingBy(Comanda::oras, TreeMap::new, Collectors.summingDouble(Comanda::valoare)));
        System.out.println(totalPeOras);

        long mari = comenzi.stream().filter(c -> c.valoare() >= 100).count();
        System.out.println("comenzi de cel puțin 100 lei: " + mari);

        System.out.println("pătrate: " + IntStream.rangeClosed(1, 5).map(i -> i * i).boxed().toList());
    }
}

Rezultat

{Brașov=200.0, Cluj=140.0, Iași=165.5}
comenzi de cel puțin 100 lei: 2
pătrate: [1, 4, 9, 16, 25]

Greșeli frecvente

  • Refolosești un stream după operația finală: IllegalStateException: stream has already been operated upon or closed. Creează altul din sursă.
  • Scrii un lanț fără operație finală și te miri că nu se întâmplă nimic. Operațiile intermediare sunt leneșe.
  • Modifici colecția sursă din interiorul stream-ului (de exemplu lista.remove în forEach): rezultat imprevizibil sau ConcurrentModificationException.
  • Folosești toList() și apoi încerci să adaugi elemente: lista returnată este nemodificabilă. Pentru o listă modificabilă folosește collect(Collectors.toCollection(ArrayList::new)).

Comenzi înrudite

  • Expresii lambda: O lambda este o funcție scurtă, fără nume, scrisă ca parametri -> corp, folosită oriunde se așteaptă o interfață funcțională.
  • Referințe la metode (::): O referință la metodă (Clasa::metoda) este o lambda prescurtată care doar apelează o metodă existentă.
  • Iteratori și for-each: for-each parcurge pe rând elementele unui array sau ale oricărui Iterable; un Iterator permite și ștergerea în timpul parcurgerii.
  • Stream: Un șir de valori prelucrate pas cu pas: filtrezi, transformi, sortezi și aduni rezultatul, fără bucle scrise de mână.
  • Collectors: Clasa cu rețete gata făcute pentru collect: liste, mulțimi, map-uri, texte unite, grupări, numărări și medii.