Java Streams API
Cuprins
- Introducere în Streams API
- Crearea Stream-urilor
- Operații Intermediare
- Operații Terminale
- Stream-uri Specializate
- Paralelizarea Stream-urilor
- Colectori
- Exemple Practice
- Bune Practici
- Comparație cu Metodele Tradiționale
- Probleme Uzuale și Soluții
- Referințe
Introducere în Streams API
Stream API a fost introdus în Java 8 și reprezintă un mod nou și puternic de a procesa colecții de date. Un stream reprezintă o secvență de elemente asupra cărora se pot aplica diverse operații în mod agregat.
Ce este un Stream?
- Un stream nu este o structură de date, ci un flux de date care poate fi procesat
- Operează pe o sursă de date (cum ar fi colecții, array-uri, generatori etc.)
- Nu modifică sursa de date originală
- Este proiectat pentru a fi utilizat în operații de tip pipeline
- Poate fi procesate în mod secvențial sau paralel
Caracteristici cheie ale Stream-urilor
- Procesare în pipeline - operațiile pe stream-uri pot fi înlănțuite, creând un pipeline de procesare
- Procesare internă - spre deosebire de colecții care necesită iterare externă, stream-urile sunt iterate intern
- Procesare lazy - multe operații pe stream-uri nu sunt executate până când nu se întâlnește o operație terminală
- Consumabile o singură dată - un stream poate fi parcurs o singură dată; odată consumat, trebuie creat unul nou
- Paralelism fără efort - stream-urile pot fi paralelizate cu o singură operație, fără a necesita gestionarea manuală a thread-urilor
Diferența dintre Stream și Colecție
Aspect | Colecție | Stream |
---|---|---|
Scopul principal | Stocarea datelor | Procesarea datelor |
Iterare | Externă (controlată de utilizator) | Internă (controlată de stream) |
Modificarea datelor | Permite modificarea directă | Nu permite modificarea sursă |
Accesarea elementelor | Acces aleatoriu | Acces secvențial |
Consumul | Poate fi utilizat de mai multe ori | Poate fi consumat o singură dată |
Evaluare | Imediată | Lazy (amânată) |
Paralelism | Necesită cod adițional | Suport nativ (parallelStream()) |
Crearea Stream-urilor
Există mai multe moduri de a crea stream-uri:
1. Din Colecții
List<String> lista = Arrays.asList("Java", "Python", "C++");
Stream<String> streamDinLista = lista.stream();
// Pentru stream-uri paralele
Stream<String> streamParalel = lista.parallelStream();
2. Din Array-uri
String[] array = {"Java", "Python", "C++"};
Stream<String> streamDinArray = Arrays.stream(array);
// Stream din porțiune de array
Stream<String> streamParte = Arrays.stream(array, 0, 2); // doar "Java" și "Python"
3. Din Valori Individuale
Stream<String> streamDeValori = Stream.of("Java", "Python", "C++");
4. Stream-uri Infinite
// Stream infinit de numere aleatorii
Stream<Double> randomStream = Stream.generate(Math::random);
// Stream infinit de valori incrementate
Stream<Integer> incrementStream = Stream.iterate(0, n -> n + 1);
// Stream infinit cu predicat (Java 9+)
Stream<Integer> numereSubZece = Stream.iterate(0, n -> n < 10, n -> n + 1);
5. Stream-uri Goale
Stream<String> streamGol = Stream.empty();
6. Din Fișiere (java.nio.file)
try {
Stream<String> liniiDinFisier = Files.lines(Paths.get("fisier.txt"));
// ...
} catch (IOException e) {
// tratare excepție
}
7. Din String-uri
IntStream streamCaractere = "Hello".chars();
// Split string și creare stream
Stream<String> streamCuvinte = Pattern.compile("\\s+").splitAsStream("Hello Java World");
Operații Intermediare
Operațiile intermediare sunt operații care returnează un nou stream și pot fi înlănțuite. Ele sunt lazy - nu sunt executate până când nu se întâlnește o operație terminală.
Filtrare
// Filtrare bazată pe predicat
Stream<String> filtrat = stream.filter(s -> s.startsWith("J"));
Transformare
// Transformare element cu element
Stream<String> uppercase = stream.map(String::toUpperCase);
// Transformare cu aplatizare (pentru stream-uri de stream-uri)
Stream<String> aplatizat = listaDeListe.stream().flatMap(List::stream);
Limitare
// Limitare la primele n elemente
Stream<String> primeleN = stream.limit(10);
// Ignorarea primelor n elemente
Stream<String> faraEle = stream.skip(5);
Sortare
// Sortare folosind ordinea naturală
Stream<String> sortatNatural = stream.sorted();
// Sortare cu comparator specific
Stream<String> sortatPersonalizat = stream.sorted(Comparator.comparing(String::length));
Distincte
// Elimină elementele duplicate
Stream<String> faraDuplicate = stream.distinct();
Peek
// Utilizat pentru debugging, execută o acțiune pentru fiecare element fără a modifica stream-ul
Stream<String> debug = stream.peek(System.out::println);
Conversie Tip (Map To)
// Conversie la stream primitiv
IntStream lungimi = stream.mapToInt(String::length);
Operații Terminale
Operațiile terminale consumă stream-ul și produc un rezultat. După o operație terminală, stream-ul este considerat consumat și nu mai poate fi utilizat.
Colectare
// Colectare într-o listă
List<String> lista = stream.collect(Collectors.toList());
// Colectare într-un set
Set<String> set = stream.collect(Collectors.toSet());
// Colectare într-un string
String string = stream.collect(Collectors.joining(", "));
Reducere
// Reducere cu valoare inițială și operator
int suma = stream.mapToInt(String::length).reduce(0, Integer::sum);
// Reducere fără valoare inițială (returnează Optional)
Optional<String> concatenate = stream.reduce((s1, s2) -> s1 + s2);
ForEach
// Execută o acțiune pentru fiecare element
stream.forEach(System.out::println);
// ForEach ordonat (respectă ordinea pentru stream-uri paralele)
stream.forEachOrdered(System.out::println);
Min/Max
// Găsește valoarea minimă (returnează Optional)
Optional<String> min = stream.min(Comparator.comparing(String::length));
// Găsește valoarea maximă (returnează Optional)
Optional<String> max = stream.max(Comparator.comparing(String::length));
Count
// Numără elementele din stream
long count = stream.count();
Verificare
// Verifică dacă toate elementele satisfac predicatul
boolean toateAuA = stream.allMatch(s -> s.contains("a"));
// Verifică dacă cel puțin un element satisface predicatul
boolean celPutinUnul = stream.anyMatch(s -> s.contains("a"));
// Verifică dacă niciun element nu satisface predicatul
boolean nici = stream.noneMatch(s -> s.contains("z"));
Găsire
// Găsește orice element care satisface predicatul (Optional)
Optional<String> gasit = stream.filter(s -> s.length() > 10).findAny();
// Găsește primul element care satisface predicatul (Optional)
Optional<String> primul = stream.filter(s -> s.length() > 10).findFirst();
toArray
// Conversie la array de Object
Object[] arr = stream.toArray();
// Conversie la array de tip specific
String[] arrString = stream.toArray(String[]::new);
Stream-uri Specializate
Java oferă variante specializate pentru tipurile primitive pentru a evita boxing/unboxing:
IntStream
// Creare
IntStream intStream = IntStream.range(1, 10); // 1-9
IntStream intStreamClosed = IntStream.rangeClosed(1, 10); // 1-10
// Operații specifice
int sum = intStream.sum();
OptionalDouble avg = intStream.average();
OptionalInt max = intStream.max();
OptionalInt min = intStream.min();
LongStream
LongStream longStream = LongStream.rangeClosed(1L, 1000000L);
long sum = longStream.sum();
DoubleStream
DoubleStream doubleStream = DoubleStream.of(1.1, 2.2, 3.3);
double sum = doubleStream.sum();
Conversii
// De la Stream<T> la stream primitiv
IntStream intStream = lista.stream().mapToInt(String::length);
// De la stream primitiv la Stream<T>
Stream<Integer> boxed = intStream.boxed();
Paralelizarea Stream-urilor
Stream-urile pot fi procesate paralel pentru a îmbunătăți performanța pe seturi mari de date:
// Creare stream paralel dintr-o colecție
Stream<String> parallelStream = lista.parallelStream();
// Conversie stream secvențial la paralel
Stream<String> parallelStream2 = stream.parallel();
// Verificare dacă un stream este paralel
boolean isParallel = stream.isParallel();
// Conversie stream paralel la secvențial
Stream<String> sequentialStream = parallelStream.sequential();
Considerații pentru stream-uri paralele:
- Utile pentru seturi mari de date
- Eficiente pentru operații costisitoare
- Pot fi mai lente decât stream-urile secvențiale pentru seturi mici de date
- Ordinea nu este garantată (cu excepția
forEachOrdered
) - Necesită operații stateless și non-interfering
- Performanța depinde de hardware (număr de core-uri CPU)
Colectori
Clasa Collectors
oferă numeroase metode factory pentru operațiuni comune de reducere:
Colectori de bază
// Colectare în colecții
List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
Collection<String> coll = stream.collect(Collectors.toCollection(LinkedList::new));
// Conversie la String
String joined = stream.collect(Collectors.joining(", "));
Agregări
// Numărare
long count = stream.collect(Collectors.counting());
// Sumă, medie, minim, maxim
int sum = stream.collect(Collectors.summingInt(String::length));
double avg = stream.collect(Collectors.averagingInt(String::length));
Optional<String> min = stream.collect(Collectors.minBy(Comparator.naturalOrder()));
// Statistici
IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(String::length));
System.out.println("Count: " + stats.getCount());
System.out.println("Min: " + stats.getMin());
System.out.println("Max: " + stats.getMax());
System.out.println("Sum: " + stats.getSum());
System.out.println("Average: " + stats.getAverage());
Grupare și Partiționare
// Grupare după lungime
Map<Integer, List<String>> grupate =
stream.collect(Collectors.groupingBy(String::length));
// Grupare și transformare downstream
Map<Integer, Set<String>> grupateInSet =
stream.collect(Collectors.groupingBy(String::length, Collectors.toSet()));
// Grupare cu calcul statistic
Map<Integer, Double> avgLengthByFirstChar =
stream.collect(Collectors.groupingBy(
s -> s.charAt(0),
Collectors.averagingInt(String::length)));
// Partiționare (grupare binară bazată pe predicat)
Map<Boolean, List<String>> partitionat =
stream.collect(Collectors.partitioningBy(s -> s.length() > 5));
Colectori compuși
// Grupare cu numărare
Map<Character, Long> countByFirstChar =
stream.collect(Collectors.groupingBy(
s -> s.charAt(0),
Collectors.counting()));
// Grupare cu joining
Map<Integer, String> stringsByLength =
stream.collect(Collectors.groupingBy(
String::length,
Collectors.joining("-")));
Exemple Practice
Exemplul 1: Filtrare și Transformare
List<String> rezultat = lista.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
Exemplul 2: Procesare de Obiecte
class Produs {
private String nume;
private double pret;
private String categorie;
// constructor, getteri, setteri
}
List<Produs> produse = // ...
// Suma prețurilor produselor dintr-o anumită categorie
double total = produse.stream()
.filter(p -> "Electronice".equals(p.getCategorie()))
.mapToDouble(Produs::getPret)
.sum();
// Produsul cel mai scump per categorie
Map<String, Optional<Produs>> celMaiScump = produse.stream()
.collect(Collectors.groupingBy(
Produs::getCategorie,
Collectors.maxBy(Comparator.comparing(Produs::getPret))));
Exemplul 3: Procesare Numerică
// Generare și procesare numere
List<Integer> numere = IntStream.rangeClosed(1, 100)
.boxed()
.collect(Collectors.toList());
// Numere prime
List<Integer> prime = numere.stream()
.filter(n -> n > 1 && IntStream.range(2, n)
.noneMatch(i -> n % i == 0))
.collect(Collectors.toList());
// Suma pătratelor numerelor pare
int sumaPătrate = numere.stream()
.filter(n -> n % 2 == 0)
.mapToInt(n -> n * n)
.sum();
Exemplul 4: Operațiuni pe Fișiere
// Numărare cuvinte unice într-un fișier
try {
long cuvinte = Files.lines(Paths.get("fisier.txt"))
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.map(String::toLowerCase)
.distinct()
.count();
System.out.println("Număr de cuvinte unice: " + cuvinte);
} catch (IOException e) {
e.printStackTrace();
}
Exemplul 5: Grouping Complex
// Grupare studenți după calificativ și calcul statistici
class Student {
private String nume;
private int varsta;
private double nota;
// constructor, getteri, setteri
public String getCalificativ() {
if (nota >= 9) return "Excelent";
if (nota >= 7) return "Bine";
if (nota >= 5) return "Satisfăcător";
return "Nesatisfăcător";
}
}
List<Student> studenti = // ...
Map<String, DoubleSummaryStatistics> statisticiPeCalificative = studenti.stream()
.collect(Collectors.groupingBy(
Student::getCalificativ,
Collectors.summarizingDouble(Student::getNota)));
Bune Practici
- Folosiți operații intermediare pentru a reduce dimensiunea stream-ului cât mai devreme posibil
// Bine - filtrează mai întâi, apoi transformă stream.filter(s -> s.length() > 5).map(expensiveOperation); // Mai puțin eficient - transformă toate elementele, apoi filtrează stream.map(expensiveOperation).filter(s -> s.length() > 5);
- Evitați operațiile cu efecte secundare în stream-uri
// Evitați - modifică o variabilă externă List<String> rezultate = new ArrayList<>(); stream.forEach(s -> rezultate.add(s.toUpperCase())); // NU FACEȚI ASTA // Corect - folosiți collect List<String> rezultate = stream.map(String::toUpperCase).collect(Collectors.toList());
- Folosiți stream-uri specializate pentru tipuri primitive
// Mai eficient - evită boxing/unboxing int sum = IntStream.rangeClosed(1, 1000).sum(); // Mai puțin eficient int sum = Stream.iterate(1, n -> n + 1) .limit(1000) .mapToInt(Integer::intValue) .sum();
- Aveți grijă la stream-uri infinite
// Corect - limitează stream-ul infinit Stream.iterate(0, n -> n + 1) .limit(100) .forEach(System.out::println); // GREȘIT - va executa la infinit // Stream.iterate(0, n -> n + 1).forEach(System.out::println);
- Folosiți parallel() cu precauție
// Potrivit pentru paralelizare - operație costisitoare pe date mari List<String> rezultat = lista.parallelStream() .filter(s -> s.length() > 3) .map(s -> procesareCostisitoare(s)) .collect(Collectors.toList()); // Nu se justifică paralelizarea - operație simplă pe date puține List<String> rezultat = lista.stream() // fără parallelStream() .filter(s -> s.length() > 3) .map(String::toUpperCase) .collect(Collectors.toList());
- Folosiți colectori predefiniti când este posibil
// Mai expresiv și probabil mai eficient Map<Boolean, List<String>> partitionat = stream.collect(Collectors.partitioningBy(s -> s.length() > 5)); // Mai puțin expresiv Map<Boolean, List<String>> manual = new HashMap<>(); manual.put(true, new ArrayList<>()); manual.put(false, new ArrayList<>()); stream.forEach(s -> { if (s.length() > 5) { manual.get(true).add(s); } else { manual.get(false).add(s); } });
- Împărțiți lanțurile lungi de stream-uri pentru lizibilitate
// Mai greu de citit List<String> rezultat = persoane.stream() .filter(p -> p.getVarsta() > 18) .map(Persoana::getNume) .map(String::toUpperCase) .filter(n -> n.startsWith("A")) .sorted() .limit(10) .collect(Collectors.toList()); // Mai ușor de citit și înțeles Stream<Persoana> adulti = persoane.stream() .filter(p -> p.getVarsta() > 18); Stream<String> nume = adulti .map(Persoana::getNume) .map(String::toUpperCase); List<String> rezultat = nume .filter(n -> n.startsWith("A")) .sorted() .limit(10) .collect(Collectors.toList());
Comparație cu Metodele Tradiționale
Iterare Tradițională vs Stream
// Abordare tradițională (iterare externă)
List<String> rezultat = new ArrayList<>();
for (String s : lista) {
if (s.length() > 3) {
String upper = s.toUpperCase();
rezultat.add(upper);
}
}
// Abordare cu stream (iterare internă)
List<String> rezultat = lista.stream()
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.collect(Collectors.toList());
Avantajele Stream-urilor
- Cod mai concis și declarativ
- Operațiile pot fi paralelizate ușor
- Lazy evaluation - poate îmbunătăți performanța
- Compoziție mai simplă a operațiilor
- Mai puțin cod boilerplate - mai puține variabile temporare
Dezavantaje ale Stream-urilor
- Pot fi mai greu de debugat
- Pot avea o ușoară penalizare de performanță pentru operații simple
- Curba de învățare pentru programatori obișnuiți cu stilul imperativ
Probleme Uzuale și Soluții
Problema 1: Stream-uri consumate
Stream<String> stream = lista.stream();
stream.forEach(System.out::println);
// Următoarea linie va arunca o excepție
stream.count(); // IllegalStateException: stream has already been operated upon or closed
Soluție: Creați un nou stream când aveți nevoie să procesați din nou aceleași date.
Problema 2: Operații stateful
// Poate avea comportament nedorit în stream-uri paralele
List<String> rezultat = lista.parallelStream()
.sorted() // operație stateful costisitoare în stream-uri paralele
.collect(Collectors.toList());
Soluție: Evaluați dacă paralelizarea este cu adevărat necesară. Uneori, un stream secvențial cu operații stateful este mai eficient.
Problema 3: Efecte secundare
// Anti-pattern: efecte secundare în operații de stream
List<Integer> numere = new ArrayList<>();
IntStream.range(1, 10).forEach(numere::add); // Efect secundar
// Abordare corectă
List<Integer> numere = IntStream.range(1, 10).boxed().collect(Collectors.toList());
Problema 4: Stream-uri și excepții verificate
// Nu compilează - lambda-urile nu pot arunca excepții verificate
List<String> linii = Files.list(Paths.get("director"))
.map(path -> Files.readAllLines(path)) // Compile error: excepție verificată
.collect(Collectors.toList());
Soluție: Încapsulați codul care poate arunca excepții într-o metodă separată sau folosiți un wrapper pentru excepții:
List<List<String>> linii = Files.list(Paths.get("director"))
.map(path -> {
try {
return Files.readAllLines(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
})
.collect(Collectors.toList());