Programare Functionala in Java
Programare 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.
● 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
@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);
● 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.
● 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.
● Astfel, pentru interfața funcțională definită anterior, ambele implementări folosind expresii
lambada sunt valide:
● 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:
● 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.
● Consumers
● Predicate
○ În exemplul de mai jos am creat un predicate care verifică dacă un număr este impar:
● Function
● Operators
○ În exemplul de mai jos primim un String și îl întoarcem, dar cu prima literă mare și
restul mici:
● 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:
● 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.
● 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.
● 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
● 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.
● count()
○ Această operație întoarce numărul de elemente din stream. Astfel, în exemplul de mai
jos, va întoarce numărul de cursuri:
● 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
● 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
● Collect este o operație terminală folosită pentru a acumula conținutul unui stream într-un
container precum o colecție.
● 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:
● 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:
● 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:
● 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ă)
● 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:
● După ce am făcut o operație de grupare, putem aplica încă o colectare pentru fiecare grup
format.
● Un exemplu este mai jos, unde afișăm lungimile distincte pentru nume, și, pentru fiecare
lungime, numărul de nume care au acea lungime:
● 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).
● 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)
○ 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":
● limit(n)
○ 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)
○ 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()
○ În exemplul următor vom folosi această operație pentru a elimina cursurile duplicate:
● sorted()
● map(Function)
● 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.
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.
4. Să se afișeze doar cărțile lui Mike, cu prețul mai mare decât 50.
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
● 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
○ range(start, end) → pentru generarea unui stream cu valorile din intervalul [start,end)
LongStream.rangeClosed(10, 100)
.filter(el -> el % 10 == 0)
.forEach(System.out::println);
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.
● 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.
● Astfel, utilizarea clasei Optional duce la evitarea NullPointerException, obținută atunci când se
apelează o metodă pe un obiect care este null.
Optional
● 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:
● filter(Predicate)
○ Mai jos, conditia de filtrare este ca numarul de caractere sa fie mai mare decat 4
● 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
● 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.
● După cum se poate observa, Optional este o clasă generică, deci se poate utiliza numai cu
obiecte.
● Astfel, dacă vrem să aflăm media primelor 100 de numere naturale pozitive, putem folosi
următoarea secvență de co
● 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 :)