MySQL Java JavaScript PHP Python HTML-CSS C-sharp

Introducere în Stream API

Începând cu JDK 8, în Java a apărut un nou API numit Stream API. Scopul acestuia este să simplifice lucrul cu seturile de date, în special operațiile de filtrare, sortare și alte manipulări de date. Toată funcționalitatea principală a acestui API este concentrată în pachetul java.util.stream.

Conceptul cheie în Stream API este fluxul de date. Termenul „flux” este destul de utilizat în programare, în special în Java. În capitolele anterioare, a fost discutat lucrul cu fluxuri de caractere și byte pentru citirea și scrierea fișierelor. În contextul Stream API, fluxul reprezintă un canal de transmitere a datelor dintr-o sursă de date. Sursele pot fi fișiere, dar și tablouri sau colecții.

Un aspect distinctiv al Stream API este utilizarea expresiilor lambda, care permit reducerea semnificativă a complexității codului pentru acțiunile executate.

Un exemplu simplu poate ilustra aceasta. Să presupunem că avem o sarcină: să găsim numărul tuturor elementelor dintr-un tablou care sunt mai mari decât 0. Înainte de JDK 8, codul ar fi arătat astfel:

int[] numbers = {-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5};
int count = 0;
for(int i : numbers){
   if(i > 0) count++;
}
System.out.println(count);

Acum, aplicând Stream API:

import java.util.stream.*;
//.......................
long count = IntStream.of(-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5).filter(w -> w > 0).count();
System.out.println(count);

În loc de un ciclu și mai multe construcții condiționale, pe care le-am fi folosit înainte de JDK 8, acum putem scrie un lanț de metode care vor realiza aceleași acțiuni.

Operări terminale și intermediare

Este important să înțelegem că toate operațiile cu fluxuri sunt fie terminale, fie intermediare. Operațiile intermediare returnează un flux transformat. De exemplu, metoda filter din exemplul de mai sus preia un flux de numere și returnează un flux filtrat, cu doar numere mai mari de 0. Acestui flux returnat i se pot aplica și alte operații intermediare.

Operațiile terminale returnează un rezultat concret. De exemplu, metoda count() din exemplul anterior este o operație terminală și returnează un număr. După aceasta, nu mai pot fi aplicate alte operații intermediare.

Executare amânată

Toate fluxurile efectuează calcule doar atunci când se aplică o operație terminală, ceea ce înseamnă că se aplică o execuție amânată.

Interfața BaseStream

La baza Stream API stă interfața BaseStream. Definiția completă a acesteia este:

interface BaseStream<T, S extends BaseStream<T, S>>

Parametrul T reprezintă tipul de date din flux, iar S este tipul fluxului, care moștenește interfața BaseStream.

Interfața BaseStream definește funcționalitatea de bază pentru lucrul cu fluxuri, implementată prin următoarele metode:

  • void close(): închide fluxul
  • boolean isParallel(): returnează true dacă fluxul este paralel
  • Iterator<T> iterator(): returnează un iterator pentru flux
  • Spliterator<T> spliterator(): returnează un spliterator pentru flux
  • S parallel(): returnează un flux paralel (fluxurile paralele pot utiliza mai multe nuclee în arhitecturile multi-core)
  • S sequential(): returnează un flux secvențial
  • S unordered(): returnează un flux neordonat

Interfața BaseStream este moștenită de o serie de interfețe destinate creării fluxurilor specifice:

  • Stream<T>: utilizat pentru fluxuri de date care reprezintă orice tip de referință
  • IntStream: utilizat pentru fluxuri de tip int
  • DoubleStream: utilizat pentru fluxuri de tip double
  • LongStream: utilizat pentru fluxuri de tip long

La lucrul cu fluxuri care reprezintă anumite tipuri primitive - double, int, long - este mai simplu să folosești interfețele DoubleStream, IntStream, LongStream. Însă, în majoritatea cazurilor, de obicei, se lucrează cu date mai complexe, pentru care este destinat interfața Stream<T>. Să analizăm câteva dintre metodele sale:

  • boolean allMatch(Predicate<? super T> predicate): returnează true dacă toate elementele fluxului îndeplinesc condiția din predicat. Operație terminală
  • boolean anyMatch(Predicate<? super T> predicate): returnează true dacă cel puțin un element din flux îndeplinește condiția din predicat. Operație terminală
  • <R,A> R collect(Collector<? super T,A,R> collector): adaugă elementele într-un container imuabil de tipul R. T reprezintă tipul de date din fluxul apelant, iar A - tipul de date din container. Operație terminală
  • long count(): returnează numărul de elemente din flux. Operație terminală
  • Stream<T> concat​(Stream<? extends T> a, Stream<? extends T> b): unește două fluxuri. Operație intermediară
  • Stream<T> distinct(): returnează un flux în care există doar date unice de tip T. Operație intermediară
  • Stream<T> dropWhile​(Predicate<? super T> predicate): omite elementele care corespund condiției din predicat până când se întâlnește un element care nu corespunde condiției. Elementele selectate sunt returnate sub formă de flux. Operație intermediară
  • Stream<T> filter(Predicate<? super T> predicate): filtrează elementele în funcție de condiția din predicat. Operație intermediară
  • Optional<T> findFirst(): returnează primul element din flux. Operație terminală
  • Optional<T> findAny(): returnează primul element întâlnit din flux. Operație terminală
  • void forEach(Consumer<? super T> action): pentru fiecare element se execută acțiunea action. Operație terminală
  • Stream<T> limit(long maxSize): păstrează în flux doar maxSize elemente. Operație intermediară
  • Optional<T> max(Comparator<? super T> comparator): returnează elementul maxim din flux. Pentru compararea elementelor se folosește comparatorul comparator. Operație terminală
  • Optional<T> min(Comparator<? super T> comparator): returnează elementul minim din flux. Pentru compararea elementelor se folosește comparatorul comparator. Operație terminală
  • <R> Stream<R> map(Function<? super T,? extends R> mapper): transformă elementele de tip T în elemente de tip R și returnează un flux cu elemente de tip R. Operație intermediară
  • <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper): permite transformarea unui element de tip T în mai multe elemente de tip R și returnează un flux cu elemente de tip R. Operație intermediară
  • boolean noneMatch(Predicate<? super T> predicate): returnează true dacă niciunul dintre elementele din flux nu îndeplinește condiția din predicat. Operație terminală
  • Stream<T> skip(long n): returnează un flux din care lipsesc primele n elemente. Operație intermediară
  • Stream<T> sorted(): returnează un flux sortat. Operație intermediară
  • Stream<T> sorted(Comparator<? super T> comparator): returnează un flux sortat conform comparatorului. Operație intermediară
  • Stream<T> takeWhile​(Predicate<? super T> predicate): selectează elementele din flux cât timp acestea îndeplinesc condiția din predicate. Elementele selectate sunt returnate sub formă de flux. Operație intermediară
  • Object[] toArray(): returnează un array cu elementele din flux. Operație terminală

Deși toate aceste operații permit interacțiunea cu fluxul ca și cum ar fi un set de date asemănător unei colecții, este important să înțelegem diferența dintre colecții și fluxuri:

  • Fluxurile nu stochează elementele. Elementele utilizate în fluxuri pot fi stocate într-o colecție sau, dacă este necesar, pot fi generate direct
  • Operațiile pe fluxuri nu modifică sursa de date. Operațiile pe fluxuri doar returnează un flux nou cu rezultatele acestor operații
  • Fluxurile sunt caracterizate prin execuție întârziată. Adică, toate operațiile pe flux se execută doar atunci când se efectuează o operație terminală și se returnează un rezultat concret, nu un nou flux
← Lecția anterioară Lecția următoare →