MyCodeSchool
Stream API în Java
mycodeschool.ro
Java 8+ API

Java Streams

Procesează colecții de date elegant și eficient cu programare funcțională în Java

Începe Tutorialul Vezi Exemple
Scroll

Cuprins

Ce sunt Stream-urile?

Un Stream în Java este o secvență de elemente care suportă operații de procesare secvențiale și paralele. Introdus în Java 8, Stream API permite scrierea codului într-un stil declarativ, similar cu interogările SQL.

💡 Caracteristici cheie

Stream-urile nu stochează date, nu modifică sursa originală și sunt evaluate lazy (doar când e necesar). Pot fi folosite o singură dată.

Anatomia unui Stream Pipeline

Sursă filter() map() sorted() collect()
Caracteristică Collection Stream
Stocare date Da Nu
Modificare sursă Da Nu
Iterare Externă (for-each) Internă (automată)
Reutilizare Multiplă O singură dată
Evaluare Eager Lazy

Crearea Stream-urilor

Există multiple modalități de a crea un Stream în Java. Iată cele mai comune:

Din Colecții

List<String> lista = Arrays.asList("Java", "Python", "JavaScript");

// Creare stream din listă
Stream<String> stream = lista.stream();

// Stream paralel
Stream<String> parallelStream = lista.parallelStream();

Din Array-uri

String[] arr = {"A", "B", "C"};
Stream<String> streamArr = Arrays.stream(arr);

// Sau folosind Stream.of()
Stream<String> streamOf = Stream.of("X", "Y", "Z");

Stream-uri Generate

// Stream infinit cu generate
Stream<Double> randomNumbers = Stream.generate(Math::random)
    .limit(5);

// Stream infinit cu iterate
Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2)
    .limit(10);

// IntStream pentru range-uri
IntStream range = IntStream.range(1, 100); // 1-99
IntStream rangeClosed = IntStream.rangeClosed(1, 100); // 1-100

Din Fișiere

// Citire fișier linie cu linie
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
    lines.forEach(System.out::println);
}

Operații Intermediare

Operațiile intermediare returnează un nou stream și sunt evaluate lazy. Ele nu procesează datele până când o operație terminală nu este apelată.

filter()

Filtrează elementele pe baza unei condiții (Predicate)

map()

Transformă fiecare element folosind o funcție

flatMap()

Aplatizează stream-uri nested într-un singur stream

distinct()

Elimină duplicatele (bazat pe equals())

sorted()

Sortează elementele natural sau cu Comparator

peek()

Execută o acțiune pentru fiecare element (debugging)

limit()

Limitează numărul de elemente procesate

skip()

Sare peste primele n elemente

Exemple de Cod

List<String> nume = Arrays.asList("Ana", "Maria", "Ion", "Ana", "Bogdan");

// filter() - păstrează doar numele care încep cu 'A'
nume.stream()
    .filter(n -> n.startsWith("A"))
    .forEach(System.out::println); // Ana, Ana

// map() - convertește la uppercase
nume.stream()
    .map(String::toUpperCase)
    .forEach(System.out::println); // ANA, MARIA, ION...

// distinct() + sorted()
nume.stream()
    .distinct()
    .sorted()
    .forEach(System.out::println); // Ana, Bogdan, Ion, Maria

flatMap() în Detaliu

List<List<Integer>> listaDeListe = Arrays.asList(
    Arrays.asList(1, 2, 3),
    Arrays.asList(4, 5),
    Arrays.asList(6, 7, 8)
);

// Aplatizare în un singur stream
List<Integer> toateNumerele = listaDeListe.stream()
    .flatMap(List::stream)
    .collect(Collectors.toList());
// Rezultat: [1, 2, 3, 4, 5, 6, 7, 8]

Operații Terminale

Operațiile terminale produc un rezultat sau un efect secundar și declanșează procesarea întregului pipeline.

collect()

Colectează rezultatele într-o colecție sau alt format

forEach()

Execută o acțiune pentru fiecare element

reduce()

Combină elementele într-un singur rezultat

count()

Numără elementele din stream

findFirst() / findAny()

Returnează primul/un element ca Optional

anyMatch() / allMatch() / noneMatch()

Verifică dacă elementele satisfac o condiție

min() / max()

Găsește minimul/maximul folosind Comparator

toArray()

Convertește stream-ul într-un array

reduce() - Agregare Puternică

List<Integer> numere = Arrays.asList(1, 2, 3, 4, 5);

// Suma tuturor numerelor
int suma = numere.stream()
    .reduce(0, (a, b) -> a + b);
// sau: .reduce(0, Integer::sum)

// Produsul tuturor numerelor
int produs = numere.stream()
    .reduce(1, (a, b) -> a * b);

// Concatenare string-uri
List<String> cuvinte = Arrays.asList("Java", "este", "cool");
String propozitie = cuvinte.stream()
    .reduce("", (a, b) -> a + " " + b).trim();
// Rezultat: "Java este cool"

Operații de Matching

List<Integer> numere = Arrays.asList(2, 4, 6, 8, 10);

// Toate numerele sunt pare?
boolean toatePare = numere.stream()
    .allMatch(n -> n % 2 == 0); // true

// Există vreun număr > 5?
boolean existaMaiMare = numere.stream()
    .anyMatch(n -> n > 5); // true

// Niciunul nu e negativ?
boolean niciunNegativ = numere.stream()
    .noneMatch(n -> n < 0); // true

Collectors

Clasa Collectors oferă implementări pentru operația collect() care permit transformarea stream-urilor în diverse structuri de date.

Colectori de Bază

List<String> nume = Arrays.asList("Ana", "Ion", "Maria", "Ana");

// toList()
List<String> lista = nume.stream()
    .collect(Collectors.toList());

// toSet() - elimină duplicatele
Set<String> set = nume.stream()
    .collect(Collectors.toSet());

// joining() - concatenare
String joined = nume.stream()
    .collect(Collectors.joining(", "));
// Rezultat: "Ana, Ion, Maria, Ana"

// counting()
long count = nume.stream()
    .collect(Collectors.counting());

groupingBy() - Grupare

List<Persoana> persoane = Arrays.asList(
    new Persoana("Ana", 25, "București"),
    new Persoana("Ion", 30, "Cluj"),
    new Persoana("Maria", 25, "București"),
    new Persoana("Dan", 35, "Cluj")
);

// Grupare după oraș
Map<String, List<Persoana>> grupateOras = persoane.stream()
    .collect(Collectors.groupingBy(Persoana::getOras));

// Grupare după vârstă cu numărare
Map<Integer, Long> countByAge = persoane.stream()
    .collect(Collectors.groupingBy(
        Persoana::getVarsta,
        Collectors.counting()
    ));

// Grupare cu mapping
Map<String, List<String>> numeByOras = persoane.stream()
    .collect(Collectors.groupingBy(
        Persoana::getOras,
        Collectors.mapping(Persoana::getNume, Collectors.toList())
    ));

partitioningBy() - Partiționare

// Împărțire în două grupuri: majori și minori
Map<Boolean, List<Persoana>> majoriMinori = persoane.stream()
    .collect(Collectors.partitioningBy(p -> p.getVarsta() >= 18));

List<Persoana> majori = majoriMinori.get(true);
List<Persoana> minori = majoriMinori.get(false);

toMap() - Crearea de Maps

// Mapare nume -> persoană
Map<String, Persoana> numeToPersoana = persoane.stream()
    .collect(Collectors.toMap(
        Persoana::getNume,
        p -> p
    ));

// Cu merge function pentru duplicate
Map<String, Integer> orasToTotalVarsta = persoane.stream()
    .collect(Collectors.toMap(
        Persoana::getOras,
        Persoana::getVarsta,
        Integer::sum
    ));

Parallel Streams

Parallel streams permit procesarea datelor pe multiple thread-uri, exploatând procesoarele multi-core pentru performanță îmbunătățită.

⚠️ Atenție

Parallel streams nu sunt întotdeauna mai rapide! Overhead-ul de paralelizare poate depăși beneficiile pentru colecții mici sau operații simple.

List<Integer> numereGasit = IntStream.rangeClosed(1, 1_000_000)
    .boxed()
    .collect(Collectors.toList());

// Stream secvențial
long startSeq = System.currentTimeMillis();
long sumSeq = numereGasit.stream()
    .mapToLong(n -> n * n)
    .sum();
long timeSeq = System.currentTimeMillis() - startSeq;

// Stream paralel
long startPar = System.currentTimeMillis();
long sumPar = numereGasit.parallelStream()
    .mapToLong(n -> n * n)
    .sum();
long timePar = System.currentTimeMillis() - startPar;

System.out.println("Secvențial: " + timeSeq + "ms");
System.out.println("Paralel: " + timePar + "ms");

Când să folosești Parallel Streams

Optional & Streams

Optional este un container care poate conține sau nu o valoare non-null. Stream API îl folosește pentru a evita NullPointerException.

List<String> nume = Arrays.asList("Ana", "Ion", "Maria");

// findFirst() returnează Optional
Optional<String> primulCuA = nume.stream()
    .filter(n -> n.startsWith("A"))
    .findFirst();

// Verificare și extragere valoare
if (primulCuA.isPresent()) {
    System.out.println(primulCuA.get());
}

// Sau mai elegant cu ifPresent()
primulCuA.ifPresent(System.out::println);

// orElse() - valoare default
String rezultat = primulCuA.orElse("Niciun rezultat");

// orElseThrow() - aruncă excepție
String obligatoriu = primulCuA
    .orElseThrow(() -> new RuntimeException("Nu există!"));

// Înlănțuire cu map()
Optional<Integer> lungime = primulCuA.map(String::length);

Stream din Optional (Java 9+)

// Optional.stream() - util pentru flatMap
List<Optional<String>> optionale = Arrays.asList(
    Optional.of("Ana"),
    Optional.empty(),
    Optional.of("Ion")
);

List<String> prezente = optionale.stream()
    .flatMap(Optional::stream)
    .collect(Collectors.toList());
// Rezultat: [Ana, Ion]

Exemple Practice

Să punem în practică tot ce am învățat cu exemple reale și utile.

Exemplul 1: Procesare Listă de Produse

class Produs {
    String nume;
    String categorie;
    double pret;
    boolean inStoc;
    // constructori, getteri...
}

List<Produs> produse = getProduse();

// Top 3 cele mai scumpe produse în stoc din categoria "Electronice"
List<Produs> top3 = produse.stream()
    .filter(p -> p.isInStoc())
    .filter(p -> p.getCategorie().equals("Electronice"))
    .sorted(Comparator.comparing(Produs::getPret).reversed())
    .limit(3)
    .collect(Collectors.toList());

// Prețul mediu pe categorie
Map<String, Double> pretMediuCategorie = produse.stream()
    .collect(Collectors.groupingBy(
        Produs::getCategorie,
        Collectors.averagingDouble(Produs::getPret)
    ));

// Total valoare stoc
double totalStoc = produse.stream()
    .filter(Produs::isInStoc)
    .mapToDouble(Produs::getPret)
    .sum();

Exemplul 2: Procesare Text

String text = "Java Streams sunt foarte puternice și expresive pentru procesarea datelor";

// Număr de cuvinte
long nrCuvinte = Arrays.stream(text.split("\\s+"))
    .count();

// Cel mai lung cuvânt
Optional<String> celMaiLung = Arrays.stream(text.split("\\s+"))
    .max(Comparator.comparingInt(String::length));

// Frecvența literelor
Map<Character, Long> frecventaLitere = text.toLowerCase()
    .chars()
    .filter(Character::isLetter)
    .mapToObj(c -> (char) c)
    .collect(Collectors.groupingBy(
        c -> c,
        Collectors.counting()
    ));

Exemplul 3: Operații cu Date

import java.time.*;
import java.time.temporal.*;

// Generare listă cu ultimele 30 de zile
List<LocalDate> ultimele30Zile = Stream.iterate(
        LocalDate.now(),
        date -> date.minusDays(1)
    )
    .limit(30)
    .collect(Collectors.toList());

// Grupare comenzi după luna comenzii
Map<Month, List<Comanda>> comenziPerLuna = comenzi.stream()
    .collect(Collectors.groupingBy(
        c -> c.getData().getMonth()
    ));

Exemplul 4: Citire și Procesare Fișier

// Citire CSV și creare obiecte
List<Persoana> persoane = Files.lines(Paths.get("persoane.csv"))
    .skip(1) // sări header
    .map(line -> line.split(","))
    .map(parts -> new Persoana(
        parts[0],
        Integer.parseInt(parts[1]),
        parts[2]
    ))
    .collect(Collectors.toList());

// Căutare cuvinte într-un fișier log
long errorCount = Files.lines(Paths.get("app.log"))
    .filter(line -> line.contains("ERROR"))
    .count();

🚀 Best Practices

Folosește method references când e posibil (ex: String::toUpperCase). Preferă operații stateless și evită efectele secundare. Folosește primitive streams (IntStream, LongStream) pentru performanță.