Sunteți pe pagina 1din 74

Programarea functionala in JAVA

Continut

● Programarea functionala
● Interfete functionale
● Expresii lambda
● Streams API
● Operatii terminale
● Operatii intermediare
● Streamuri primitive
● Optional
Programarea functionala

● Java 8 a introdus mai multe functionalitati precum Stream API, Functional Interfaces, Lambda
Expressions si Optional care au reprezentat inceputul programarii functionale in Java.

● Programarea functionala este un stil de programarea declarativ care trateaza calculele ca pe


o evaluare a functiilor matematice.

● O functie este o expresie care leaga un set de intrare de un set de iesire

● Important, rezultatul (ouput-ul) unei functii depinde numai de intrara acesteia


Paradigme de programare

● Stilurile de programare pot fi clasificate in: imperative si declarative

● Programarea imperativa defineste un program ca o succesiune de instructiuni care schimba


starea programului pana cand acesta ajunge in starea finala. Se specifica, pas cu pas, ce trebuie
sa faca programul

● In contrast, programarea declarativa exprima logica de calcul, fara sa descrie fluxul de control
al acestuia sub forma unei secvente de instructiuni. Simplu spus, abordarea declarativa se
concentreaza pe definirea a ceea ce programul trebuie sa realizeze, mai degraba decat pe
modul in care ar trebui sa il realizeze.
Paradigme de programare - filtrarea numerelor pare

// imperativ
List<Integer> numere = List.of(1,2,3,4,5,6,7,8,9);
List<Integer> numerePareImperativ = new ArrayList<>();
for(Integer el : numere){
if(el % 2 == 0){
numerePareImperativ.add(el);
}
}

// declarativ
List<Integer> numere = List.of(1,2,3,4,5,6,7,8,9);
List<Integer> numerePareDeclarativ = numere.stream()
.filter(el -> el % 2 == 0)
.collect(Collectors.toList());
Interfete functionale

● Conceptul de interfață funcțională a fost introdus în Java 8.

● O interfață funcțională este o interfață care conține o singură metodă abstractă


(neimplementată). Astfel, interfețele funcționale sunt de fapt interfețe SAM (Single
Abstract Method).

● Este recomandat ca toate interfețele funcționale să aibă adnotarea @FunctionalInterface.


Aceasta are rol informativ și, de asemenea, permite compilatorului să genereze o eroare
dacă interfața adnotată nu îndeplinește condițiile.

@FunctionalInterface
public interface Logger {
void print(String input);
}
Interfete functionale

● O interfață funcțională poate avea oricâte metode statice sau default. Astfel, următoarea
interfață este o interfață funcțională validă:

@FunctionalInterface
public interface Logger {
void print(String input);

default void print(Object input) {


format(input);
}

static void format(Object input) {


System.out.println("The object is: " + input);
}
}
Expresii Lambda

● Expresiile lambda au fost adăugate de asemenea în Java 8 și reprezintă un pas important


făcut de Java către programarea funcțională.

● O expresie lambda este o funcție, un bloc scurt de cod, care poate primi ca input niște
parametrii, execută niște instrucțiuni și returnează o valoare.

● Sunt similare unei metode, dar nu au nume și pot fi scrie în interiorul unei metode, fără a
aparține unei clase.

● Acestea se bazează pe existența unei interfețe funcționale, oferind o modalitate simplă,


rapidă, scurtă și lizibilă de implementare a unei astfel de interfețe.
Expresii Lambda

● Structura generală a unei expresii lambda este descrisă mai jos:

● Parantezele corespunzătoare parametrilor pot să lipsească, dacă este folosit un singur


parametru. De asemenea, dacă specificăm tipul parametrului, sau nu avem nici
parametru, sau avem mai mulți parametri, atunci folosirea parantezelor este
obligatorie.

● Putem omite tipul parametrilor, dar dacă specificăm măcar unul, atunci trebuie sa îl scriem
pentru fiecare în parte.

● În partea dreaptă, suntem obligați să folosim acolade doar dacă folosim instrucțiunea de
return urmată de ‘;’ sau dacă avem mai multe instrucțiuni.

(parametrii) -> {bloc de instructiuni}


Expresii Lambda

● Astfel, pentru interfața funcțională definită anterior, ambele implementări folosind expresii
lambada sunt valide:

Logger simpleLogger = input -> System.out.println(input);


Logger complexLogger = (String input) -> {
System.out.print("------------------------");
System.out.println(input);
System.out.print("------------------------");
};
Expresii Lambda

● Parametrii unei expresii lambda se suprapun pe cei ai singurei metode abstracte din
interfața funcțională pe care o implementează.

● Din acest motiv, tipurile acestor parametrii pot fi omise, pentru că sunt automat inferate
din semnătura metodei. De asemenea, și tipul de return este inferat.

● După instanțierea acestor obiecte, le putem folosi ca pe obiecte normale, apeland metoda
implementata:

simpleLogger.print("Hello world!"); // Hello world!


complexLogger.print("Hello world!");
// ------------------------
// Helo world
// ------------------------
Expresii Lambda

● În cazul în care expresia lambda apelează o altă metodă cu parametrii pe care îi


primește, expresiile lambda oferă o formă mai scurtă de a face acest lucru, și anume
referințe la metoda (method references).

● Operatorul :: semnalează compilatorului că urmează o referință la metodă. Metoda


referită este cea care urmează după acest operator și este apelată pe clasă sau obiectul
care se află înainte.

● În cazul nostru, pentru simpleLogger am putea folosi următoarea sintaxă:

Logger simpleLogger = System.out::println;


Expresii Lambda

● Există mai multe tipuri de referințe:

○ Referința către o metodă statică: Clasa::metodaStatica;

○ Referința pentru o metodă de instanța pentru un obiect arbitrar: Clasa::metoda;

○ Referința pentru o metodă de instanța pentru un obiect specific: obiect::metoda;

○ Referința către un constructor: Clasa::new.


Interfete functionale built-in

● Pachetul java.util.function conține mai multe interfețe funcționale generice, iar în


continuare le vom parcurge pe cele mai importante.

● Supplier

○ Nu primește nici un parametru și are rolul de a genera o valoare. Mai jos avem un
supplier care întoarce o valoare de tip double, random.

○ Se apeleaza metoda get pentru obtinerea valorii

Supplier<Double> doubleSupplier = () -> Math.random() * 100;


System.out.println(doubleSupplier.get());
Interfete functionale built-in

● Consumers

○ Consumer<T> primește o valoare de tipul generic specificat și nu întoarce nimic.


Scopul acestuia este de a consuma inputul.

○ BiConsumer<T, G> similar cu Consumer<T>, doar că primește două tipuri generice pe


care le consumă.

○ Pentru acest exemplu am folosit o referință la metodă.

○ Metoda folosita este accept

Consumer<String> logger = System.out::println;


logger.accept("Hello world!")
Interfete functionale built-in

● Predicate

○ Predicate<T> primește o valoare de tipul T și returnează true sau false.

○ BiPredicate<T, G> similar, dar primește 2 tipuri generice.

○ Metoda folosita pentru predicate este test

○ În exemplul de mai jos am creat un predicate care verifică dacă un număr este impar:

Predicate<Integer> isOdd = value -> value % 2 == 1;


System.out.println(isOdd.test(23));
Interfete functionale built-in

● Function

○ Function<T,R> primește un parametru de tip T și returnează o valoare de tip R.

○ BiFunction<T,G,R> similar, dar primește două valori, de tip T și G si returnează o


valoare de tip R.

○ Metoda folosita este apply

○ În exemplul de mai jos primim ca input un String și returnăm numărul de caractere,


sub forma unui Integer:

Function<String, Integer> nrOfCharacters = input -> input.length();


System.out.println(nrOfCharacters.apply("input"));
Interfete functionale built-in

● Operators

○ UnaryOperator<T>, similar cu Function<T,T>, primește ca input o valoare de tip T și


întoarce o valoare de același tip.

○ BinaryOperator<T>, similar cu BiFunction<T,T,T>

○ Folosim metoda apply

○ În exemplul de mai jos primim un String și îl întoarcem, dar cu prima literă mare și
restul mici:

UnaryOperator<String> properCaseOperator = (input) ->


input.substring(0, 1).toUpperCase() + input.substring(1).toLowerCase();
System.out.println(properCaseOperator.apply("alex!"));
Interfete functionale primitive built-in

● Cum o primitivă nu poate fi folosită pentru un tip generic, există variante speciale ale acestor
interfețe funcționale pentru principalele tipuri de primitive: double, int, long și combinații ca tip
de return. Astfel avem:

○ IntFunction, DoubleFunction, LongFunction: tipul de return este generic.

○ ToIntFunction, ToDoubleFunction, ToLongFunction: tipul pentru input este generic, cel


de return fiind specificat din numele interfeței.

○ DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction,


IntToLongFunction, LongToIntFunction, LongToDoubleFunction: atât tipul de input, cât
și cel de return sunt specificate din nume.
Interfete functionale primitive built-in

○ BooleanSupplier, IntSupplier, DoubleSupplier, LongSupplier: întorc o primitivă.

○ IntConsumer, DoubleConsumer, LongConsumer: consumă o primitivă.

○ IntPredicate, DoublePredicate, LongPredicate: variante predicate pentru primitive.

○ DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator,


DoubleBinaryOperator, IntBinaryOperator, LongBinaryOperator.
Exemple

1. Să se instanțieze, folosind expresii lambda, un obiect de tip Supplier<Integer> care


produce un număr între 0 și anul vostru de naștere.

2. Să se scrie o instanță de Function<String, Integer> care ia ca parametru un String ce


reprezintă un număr și întoarce pătratul acestuia. De exemplu, inputul va fi "9", iar
rezultatul va fi 81.

3. Să se scrie o instanță de Predicate<Integer> care verifică dacă un număr este prim. Un


număr este prim dacă are doar doi divizori (1 și pe el însuși). Dacă inputul va fi 5,
rezultatul va fi true.
Stream

● Una dintre cele mai importante funcționalități adăugate în Java 8 a fost Stream API, compus
din clase aflate în pachetul java.util.stream.

● Un Stream este o modalitate de procesare a unui set de date aplicând o serie


înlănțuită de operații.

● Acesta nu este o structură de date, dar își ia inputul dintr-o colecție/array sau un fișier. Un
stream trebuie să aibă o sursă de date în mod obligatoriu, care poate fi de 2 tipuri: finită și
infinită.

● Din aceasta sursă pleacă datele pe un pipeline, unde pot ajunge la diverse operații
intermediare care pot fi înlănțuite. Operațiile intermediare de pe un stream sunt evaluate
lazy, ceea ce înseamnă că nu vor fi executate până nu se aplică operația terminală, a cărei
prezență este obligatorie.

● Cum stream-ul poate fi folosit o singură dată, acesta nu mai este valid după aplicarea
operației terminale.
Stream

● Una dintre cele mai importante funcționalități adăugate în Java 8 a fost Stream API, compus
din clase aflate în pachetul java.util.stream.

● Un Stream este o modalitate de procesare a unui set de date aplicând o serie


înlănțuită de operații.

● Acesta nu este o structură de date, dar își ia inputul dintr-o colecție/array sau un fișier. Un
stream trebuie să aibă o sursă de date în mod obligatoriu, care poate fi de 2 tipuri: finită și
infinită.

● Din aceasta sursă pleacă datele pe un pipeline, unde pot ajunge la diverse operații
intermediare care pot fi înlănțuite. Operațiile intermediare de pe un stream sunt evaluate
lazy, ceea ce înseamnă că nu vor fi executate până nu se aplică operația terminală, a cărei
prezență este obligatorie.

● Cum stream-ul poate fi folosit o singură dată, acesta nu mai este valid după aplicarea
operației terminale.
Stream - modalitati de creare

● Clasa centrala este Stream<T>

● Pentru stream-uri finite avem următoarele variante de instanțiere:

○ Stream.empty() → întoarce un stream gol;

○ Stream.of(T...) → întoarce un stream cu elementele trimise ca parametri;

○ Arrays.stream(T[]) → întoarce un stream cu elementele array-ului trimis ca parametru;

○ collectionInstance.stream() → întoarce un stream cu elementele colecției pe care e


aplicată metoda stream().

Stream<Object> emptyStream = Stream.empty();


Stream<String> stringStream = Stream.of("Java", "1P", "course");
List<Integer> numbers = List.of(1,2,3,4,5,6,7,8,9);
Stream<Integer> integerStream = numbers.stream();
Stream - modalitati de creare

● Pentru stream-uri infinite avem două posibilități:

○ Stream.iterate(start, UnaryOperator) → întoarce un stream infinit cu obiectele


obținute prin aplicarea funcției descrise de operatorul unar, recursiv, elementului de
start (start, f(start), f(f(start), ...)).

○ Stream.generate(Supplier) → întoarce un stream infinit cu elemente generate cu


ajutorul supplier-ului trimis ca parametru.

Stream<Integer> evenNumbers = Stream.iterate(0, el -> el + 2);


Stream<Double> randomNumbers = Stream.generate(() -> Math.random() * 100);
Stream - operatii terminale

● Așa cum am spus anterior, operațiile terminale sunt cele care produc un rezultat. Acesta este
motivul pentru care vom începe cu ele, pentru a putea vizualiza rezultatele și a înțelege astfel
mai bine ce fac respectivele operații.

● forEach(Consumer)

○ Este cea mai simplă și comună operație terminală. Acesta iterează peste toate
elementele unui stream și aplică consumatorul primit pentru fiecare, fără a returna
ceva.

○ Un exemplu de utilizare a acestei metode este afișarea conținutului unui stream.

Stream<String> javaCourses = Stream.of("Java1", "Java2", "Java3", "Java4");


javaCourses.forEach(el -> System.out.println(el));
Stream - operatii terminale

● count()
○ Această operație întoarce numărul de elemente din stream. Astfel, în exemplul de mai
jos, va întoarce numărul de cursuri:

long coursesNumber = Stream.of("Java1", "Java2", "Java3", "Java4")


.count();
System.out.println(coursesNumber); //4
Stream - operatii terminale

● allMatch(Predicate), anyMatch(Predicate), noneMatch(Predicate)

● Aceste metode primesc ca parametru un predicate și returnează o valoare de tip boolean cu


următoarea semnificație:

○ allMatch() → dacă pentru toate elementele de pe stream predicatul a returnat true,


atunci se întoarce true; altfel false.
○ anyMatch() → dacă pentru cel puțin un element de pe stream predicatul a returnat
true, se întoarce true; altfel false.
○ noneMatch() → se întoarce true dacă predicatul nu a returnat true pentru nici un
element, altfel false.

boolean areAllNumbersGreaterThan2 = List.of(12, 1, 5, 8, 16).stream().allMatch(el -> el > 2); // false


boolean isAtLeastOneNumberGreaterThan2 = List.of(12, 4, 5, 8, 16).stream().anyMatch(el -> el > 2); //true
boolean areAllNumbersSmallerThan2 = List.of(12, 4, 5, 8, 16).stream().noneMatch(el -> el > 2); //false
Stream - operatii terminale

● min(Comparator), max(Comparator)

○ Aceste două operații primesc un comparator (il putem trimite ca expresie lambda) ca
parametru și întorc cel mai mic, respectiv cel mai mare obiect din stream conform acelui
comparator.
○ De menționat că aceste metode întorc un Opțional, despre care vom vorbi mai tarziu

Optional<String> longestName = Stream.of("Alex", "Diana", "John", "Donald")


.max((name1, name2) -> name1.length() - name2.length());
System.out.println(longestName.get()); //Donald

Optional<String> firstNameAlphabeticallyOrder = Stream.of("Alex", "Diana", "John", "Donald")


.min((name1, name2) -> name1.compareTo(name2));
System.out.println(firstNameAlphabeticallyOrder.get()); //Alex
Stream - operatii terminale

● reduce(BinaryOperator)

○ Această operație are 3 forme, celelalte două primind parametrii suplimentari pe lângă
operatorul binar, operator folosit pentru a agrega toate elementele streamului.

○ O altă formă poate fi folosită pentru calcularea sumei unui stream de numere întregi. Vom
folosi varianta de reduce care primeste si un acumulator pe langa operatorul binar

Optional<String> reducedValue = Stream.of("Alex", "Diana", "John", "Donald")


.reduce((name1, name2) -> name1 + "," + name2);
System.out.println(reducedValue.get()); // Alex,Diana,John,Donald

List<Integer> oddNumbers = List.of(1,3,5,7,9);


Integer sum = oddNumbers.stream().reduce(0, (el1, el2) -> el1 + el2);
System.out.println(sum); //25
Stream - Operatia Collect

● Collect este o operație terminală folosită pentru a acumula conținutul unui stream într-un
container precum o colecție.

● Această metodă ia ca parametru un Collector, care reprezintă regula aplicată pentru


colectarea valorilor.

● Collectorii cei mai utilizați sunt definiți în clasa utilitară Collectors. În exemplul de mai jos
folosim doi collectori pentru a scoate elementele unui stream mai întâi într-o lista, apoi într-
un set:

List<String> coursesList = Stream.of("Java1", "Java1", "Java2", "Java3", "Java4")


.collect(Collectors.toList());
// coursesList = [Java1, Java1, Java2, Java3, Java4]
Set<String> coursesSet = Stream.of("Java1", "Java1", "Java2", "Java3", "Java4")
.collect(Collectors.toSet());
// coursesSet = [Java2, Java3, Java4, Java1]
Stream - Operatia Collect

● Pe caz general, este posibil să salvăm elementele în orice tip de colecție, folosind metoda
statică toCollection(), căreia să îi pasăm, sub forma de supplier, constructorul pentru tipul
de colecție dorit.

● De exemplu, dacă dorim să salvăm cursurile într-un Set, dar care să fie ordonat, atunci vom
folosi un TreeSet. Mai jos vom folosi referința la constructor:

TreeSet<String> courses = Stream.of("Java1", "Java1", "Java4", "Java3", "Java2")


.collect(Collectors.toCollection(TreeSet::new));
// courses = [Java1, Java2, Java3, Java4]
Stream - Operatia Collect

● De asemenea, există cazuri când ne dorim să scoatem informațiile din stream sub forma
unui map. Pentru acest caz avem în clasa Collectors metoda statică toMap(keyMapper,
valueMapper) unde:

○ keyMapper: mapează elementul curent de pe stream la o cheie într-un map.


○ valueMapper: mapează elementul curent de pe stream la valoarea asociată cheii de
mai devreme.

● Mai jos, colectam elementele unui stream într-un map, unde cheia este elementul (putem
folosi funcția identitate), iar valoarea este reprezentată de numărul de caractere (putem
folosi referința la metodă)

Map<String, Integer> lengthByName = Stream.of("Alex", "Diana", "John", "Donald")


.collect(Collectors.toMap(e -> e, e -> e.length()));
//lengthByName = {Diana=5, Alex=4, John=4, Donald=6}
Stream - Operatia Collect

● Tot un obiect de tip map are ca rezultat și un colector de tip groupingBy(), acesta grupând
elementele după criteriul specificat de funcția primită.

● Astfel, rezultatul va fi un map în care cheia este criteriul după care s-a făcut gruparea, iar
valoarea asociată va fi o lista cu elementele din acea categorie.

● Pentru exemplificare vom folosi același input, dar de această dată vom grupa numele după
lungimea lor:

Map<Integer, List<String>> namesByLength = Stream.of("Alex", "Diana", "John", "Donald")


.collect(Collectors.groupingBy(String::length));
//namesByLenght = {4=[Alex, John], 5=[Diana], 6=[Donald]}
Stream - Operatia Collect

● După ce am făcut o operație de grupare, putem aplica încă o colectare pentru fiecare grup
format.

● Această colectare se numește down-stream collector.

● Un exemplu este mai jos, unde afișăm lungimile distincte pentru nume, și, pentru fiecare
lungime, numărul de nume care au acea lungime:

Map<Integer, Long> numbersOfNamesWithSameLengthByLength = Stream.of("Alex",


"Diana", "John", "Donald")
.collect(Collectors.groupingBy(String::length, Collectors.counting()));
// numbersOfNamesWithSameLengthByLength = {4=2, 5=1, 6=1}
Stream - Operatia Collect

● Tot un map returnează și metoda paritioningBy().

● Aceasta primește ca parametru un Predicate și împarte elementele în două categorii: cele


pentru care predicatul a returnat true și cele pentru care a returnat false.

● De exemplu, putem împărți în numere pare și numere impare:

Map<Boolean, List<Integer>> numbersByParity = Stream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)


.collect(Collectors.partitioningBy(e -> e % 2 == 0));
// numbersByParity = {false = {1,3,5,7,9}, true = {0,2,4,6,8}}
Stream - Operatia Collect

● De menționat este și metoda joining() care preia inputul dintr-un stream de stringuri și le
concatenează, folosind delimitatorul specificat (sau nici unul dacă nu îl trimitem ca
parametru).

String allCoursesConcatenated = Stream.of("Java1", "Java2", "Java3", "Java4")


.collect(Collectors.joining(","));
// allCoursesConcatenated = Java1,Java2,Java3,Java4
Stream - Operatii intermediare

● Operațiile intermediare sau cele non-finale sunt operații care transformă sau filtrează
elementele de pe stream.

● Astfel, după aplicarea unei astfel de operați, streamul va avea o formă nouă.

● Este important de menționat ca aceste operații sunt evaluate în mod lazy, doar atunci când
se folosește o operație terminală pe stream.
Stream - Operatii intermediare

● filter(Predicate)

○ Această operație este folosită pentru a filtra elementele de pe stream.

○ Aceasta primește ca parametru un predicat care va fi apelat pentru fiecare intrare, iar
dacă rezultatul este true, atunci elementul va trece mai departe, altfel nu.

○ În exemplul de mai jos vrem să afișăm doar numele care încep cu "D":

Stream.of("Alex", "Diana", "John", "Donald")


.filter(el -> el.startsWith("D"))
.forEach(System.out::println); // Diana, Donald
Stream - Operatii intermediare

● limit(n)

○ Această operație va crea un stream doar cu primele n elemente.

○ Astfel, în exemplul următor vom folosi un stream infinit și vom afișa primii 10 multiplii
ai lui 3:

Stream.iterate(0, e -> e + 3 )
.limit(10)
.forEach(System.out::println);
// 0,3,6,9,12,15,18,21,24,27
Stream - Operatii intermediare

● takeWile(Predicate)

○ Aceasta este de asemenea o operație de limitare.

○ takeWhile() păstrează elementele de pe stream cât timp, pentru niciunul dintre ele,
predicatul nu returnează false. În momentul în care valoarea false a fost returnată,
nici un alt element nu va trece mai departe.

○ În exemplul de mai jos vom folosi această operație pentru a afișa toate puterile lui 3
mai mici decât 1000.

Stream.iterate(1, e -> e * 3 )
.takeWhile(e -> e < 1000)
.forEach(System.out::println);
// 1,3,9,27,81,243,729
Stream - Operatii intermediare

● distinct()

○ Această operație păstrează pe stream doar elementele unice.

○ În exemplul următor vom folosi această operație pentru a elimina cursurile duplicate:

Stream.of("Java1", "Java1", "Java2",


"Java3", "Java4", "Java3")
.distinct()
.forEach(System.out::println);
// Java1, Java2, Java3, Java4
Stream - Operatii intermediare

● sorted()

○ Această operație sortează elementele din stream conform ordinii naturale.

○ Dacă vrem o altă ordine, putem trimite un Comparator.

○ În exemplul de mai jos afișăm numele în ordinea crescătoare a lungimii lor

Stream.of("Alex", "Diana", "John", "Donald")


.sorted((a,b) -> a.length() - b.length())
.forEach(System.out::println);
Stream - Operatii intermediare

● map(Function)

○ Transformă fiecare element de pe stream în orice alt element, conform funcției


date ca parametru.

○ În exemplul de mai jos vom scrie cu majuscule toate elementele stream-ului:

Stream.of("Java1", "Java2", "Java3", "Java4")


.map(String::toUpperCase)
.forEach(System.out::println);
// JAVA1, JAVA2, JAVA3, JAVA4
Stream - Operatii intermediare

● flatMap(Function)

○ Această operație este folosită când elementele de pe stream sunt sau conțin liste de
elemente și vrem să obținem un stream cu toate elementele din listele respective,
concatenate unele după altele.

○ În exemplul de mai jos avem un stream de liste de stringuri și vrem să afișăm orice
element din aceste liste care are lungime mai mare decât 4.

Stream.of(List.of("BMW", "Mercedes", "Audi"), List.of("Tesla", "Nio", "XPeng"))


.flatMap(el -> el.stream())
.filter(el -> el.length() > 4)
.forEach(System.out::println);
// Mercedes, Tesla, Xpeng
Exemple

1. Să se creeze clasa Book care conține:


○ String author
○ String name
○ double price

2. Există doar 3 autori: Alex, John, Mike, care împreună au 11 cărți. Numele și prețul pot fi
generate random. Să se instanțieze un stream cu cele 11 cărți.

3. Să se afișeze doar cărțile lui Alex.

4. Să se afișeze doar cărțile lui Mike, cu prețul mai mare decât 50.

5. Să se afișeze autorul cu cele mai multe cărți scrise

6. Să se afișeze fiecare autor cu cărțile lui. Dacă de exemplu Alex a scris cărtile: "Carte1A",
"Carte1B", se va afișa: Alex : Carte1A, Carte1B.
Exemple

7. Să se afișeze fiecare autor cu numărul de cărți citite. Dacă păstrăm contextul de mai sus,
se va afișa: Alex : 2

8. Din cauza inflației, prețul cărților a crescut cu 10%. John a ales însă să nu crească prețul
cărților sale, chiar dacă astea înseamnă un profit mai mic. Să se afișeze toate cărțile, cu
prețul actulizat (pentru cele scrise de Alex și Mike)
Streamuri primitive

● Stream-urile funcționează în principal cu colecții de obiecte, ci nu cu tipuri primitve. Dar,


pentru a oferi o modalitate de a folosi principalele tipuri de primitive (int, long, double), în
pachetul java.util.stream găsim 3 implementări speciale: IntStream, LongStream,
DoubleStream.

● Scopul acestor streamuri este de a oferi operații mai particulare, care nu ar putea fi aplicate
pe orice tip de obiect.

● Pentru crearea unui stream de primitive, putem folosi metode statica of(), ca în exemplul de
mai jos:

IntStream.of(1,2,3,4,5)
.forEach(System.out::println);
Streamuri primitive

● De asemenea pentru IntStream/LongStream, putem folosi metodele:

○ range(start, end) → pentru generarea unui stream cu valorile din intervalul [start,end)

○ rangeClosed(start, end) → având elementele din intervalul [start, end]

LongStream.rangeClosed(10, 100)
.filter(el -> el % 10 == 0)
.forEach(System.out::println);
Streamuri primitive

● În plus, dacă avem un stream de wrappere, putem folosi metodele mapToInt(),


mapToLong(), mapToDouble() pentru a obține stream-uri primitive.

● Pentru transformarea inversă, de la un stream de primitive la unul de obiecte avem metoda


specială boxed()

IntStream intStream = Stream.of(1, 3, 5, 7, 9)


.mapToInt(e -> e);

Stream<Long> wrapperStream = LongStream.range(10, 100)


.boxed();
Streamuri primitive

● Așa cum am spus mai sus, pe aceste stream-uri putem aplica operații speciale precum min(),
max(), sum(), average(), acestea fiind operații terminale.

● De exemplu, putem calcula suma primilor 100 de multiplii ai lui 2:

int sum = IntStream.iterate(0, el -> el + 2)


.limit(100)
.sum();
Optional

● Clasa Optional<T> este o clasă generica apărută în java 8, în pachetul java.util și are scopul
de a modela obiectele care pot fi nule.

● Acesta poate fi văzut ca un container care conține sau nu o valoare.

● Astfel, utilizarea clasei Optional duce la evitarea NullPointerException, obținută atunci când se
apelează o metodă pe un obiect care este null.
Optional

● Pentru a instanția un astfel de obiect putem folosi una dintre metodele:

○ Optional.empty(): returnează un opțional gol;

○ Optional.of(T): returnează un opțional cu valoarea primită. Dacă valoarea este null,


atunci se aruncă NullPointerException;

○ Optional.ofNullable(T): returnează un opțional care descrie o valoare care poate să fie


null.

Optional<Object> emptyOptional = Optional.empty();


Optional<String> stringOptional = Optional.of("Alex");
Optional<Object> nullableOptional = Optional.ofNullable(null);
Optional - metode

● isPresent() → întoarce true dacă este prezentă o valoare, altfel false.

● isEmpty() → întoarce true dacă nu este prezentă o valoare.

● get() → întoarce valoarea dacă există, altfel aruncă NoSuchElementException. Se recomandă


utilizarea după ce s-a verificat existența valorii.

● ifPresent(Consumer) → se execută consumatorul dacă este prezentă valoarea, altfel nu se


întâmplă nimic.

● orElse(T) → întoarce valoarea dacă este prezentă, altfel întoarce parametrul.

● orElseGet(Supplier) → similar, dacă valoarea nu este prezentă, atunci întoarce rezultatul


rulării supplier-ului

● orElseThrow() → întoarce valoare dacă există, altfel aruncă NoSuchElementException (se poate
pasa tipul de excepție care să se arunce)
Optional - metode

● Astfel, putem verifica existența valorii înainte utilizării, precum în exemplul de mai jos:

Optional<String> longestNameOptional = Stream.of("Alex", "Diana", "John", "Donald")


.max(Comparator.comparingInt(String::length));
if(longestNameOptional.isPresent()){
System.out.println(longestNameOptional.get());
}

● De asemenea, în cazul în care nu este o valoare, putem suplini absența acesteia, ca în


exemplul următor:

Optional<Double> minOptional = Stream.generate(() -> Math.random() * 100)


.limit(100)
.min(Comparator.comparingDouble(el -> el));
Double min = minOptional.orElse(0.0);
Optional - metode

● filter(Predicate)

○ Metoda se comportă similar operației de pe stream-uri.

○ Aplică predicatul primit ca parametru și dacă returnează true atunci rezultatul va fi


opționalul pe care s-a apelat metoda, altfel va fi un opțional gol.

○ Mai jos, conditia de filtrare este ca numarul de caractere sa fie mai mare decat 4

Optional<String> nameOptional = Optional.of("Alex")


.filter(el -> el.length() > 4);
System.out.println(nameOptional.get()); // NoSuchElementException
Optional - metode

● map(Function)

○ Această metodă este folosită pentru a transforma valoarea (dacă este prezentă)
conform funcției primite ca parametru

○ Mai jos, folosim o referinta la metoda ca sa convertim valoarea din optional (daca este
prezenta) in majuscule

Optional<String> upperCaseOptional = Optional.of("Alex")


.map(String::toUpperCase);
upperCaseOptional.ifPresent(System.out::println); //ALEX
Optional - metode

● stream()

○ Dacă metodele de mai sus nu sunt suficiente, putem genera un stream de la un obiect
de tip Opțional, pe care să utilizăm apoi oricare dintre operațiile învățate.

List<String> cursuri = Optional.of("curs").stream()


.map(el -> el.concat(" de java"))
.collect(Collectors.toList());
System.out.println(cursuri); //[curs de java]
Optional-uri de primitive

● După cum se poate observa, Optional este o clasă generică, deci se poate utiliza numai cu
obiecte.

● Este important de menționat că avem implementări pentru principalele tipuri de primitive:


OptionalInt, OptionalLong, OptionalDouble.

● Astfel, dacă vrem să aflăm media primelor 100 de numere naturale pozitive, putem folosi
următoarea secvență de co

OptionalDouble averageOptional = IntStream.rangeClosed(1, 100)


.average();
averageOptional.ifPresent(System.out::println); //50.5
Exemple

1. Folosind streamuri primitive, să se calculeze media primelor 100 numere naturale.

2. Se dă un stream de Stringuri. Fiecare String reprezintă un număr (ex: "99"). Să se


calculeze suma acestor numere, folosind operația terminal sum().

3. Să se creeze clasa Person care conține: String name; String phoneNumber. Să se


genereze o listă de obiecte de tip Person.

4. Să se scrie o metodă care întoarce Optional<Person> și care primește ca input un String


phoneNumber și lista de persoane. Această metoda va fi folosită pentru a căuta o
persoană după numarul de telefon.
Interviu

1. Ce functionalitati au fost adaugate in java 8?


2. Ce este o interfata functionala?
3. Exemple de interfete functionale built-in
4. Ce este o lambda expresie? Structura pentru o lambda expresie
5. Ce este un stream? Cum difera fata de o colectie?
6. Care este diferenta dintre operatiile intermediare si terminale?
7. Exemple de operatii intermediare + explicatie
8. Exemple de operatii finale + explicatie
9. Ce inseamna ca un stream este lazy?
Resurse

● https://www.baeldung.com/java-functional-programming
● https://www.baeldung.com/java-8-functional-interfaces
● https://www.geeksforgeeks.org/functional-interfaces-java/
● http://tutorials.jenkov.com/java/lambda-expressions.html
● https://www.codejava.net/java-core/collections/java-8-stream-terminal-ope
rations-examples
● http://tutorials.jenkov.com/java-functional-programming/streams.html
● https://www.baeldung.com/java-8-streams
● https://www.geeksforgeeks.org/stream-in-java/
● https://www.journaldev.com/32457/java-stream-collect-method-examples
● https://www.baeldung.com/java-8-primitive-streams
● https://www.baeldung.com/java-optional
Thank you :)

S-ar putea să vă placă și