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
| 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
- Colecții mari (100,000+ elemente)
- Operații computațional intensive
- Când ordinea nu contează
- Fără operații blocking sau I/O
- Fără stare partajată mutabilă
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ță.