This is an old revision of the document!
Scopul acestui laborator este prezentarea unei noi paradigme, concret paradigma funcțională. Aceasta vine ca un superset peste OOP în limbajul Java, tocmai pentru a facilita diverse procese.
Aspectele urmărite sunt:
Java a fost conceput inițial ca un limbaj strict orientat pe obiecte, în care:
Cu toate acestea, pe măsură ce aplicațiile au devenit:
a apărut nevoia unui stil de programare:
Începând cu Java 8, limbajul introduce:
care permit scrierea de cod într-un stil funcțional, fără a abandona modelul OOP.
În stilul imperativ, programatorul descrie exact pașii de execuție:
List<Integer> result = new ArrayList<>(); for (Integer x : numbers) { if (x % 2 == 0) { result.add(x * 2); } }
Caracteristici:
for, if)
În stilul funcțional, descriem ce transformări vrem să aplicăm, nu pașii exacți:
List<Integer> result = numbers.stream() .filter(x -> x % 2 == 0) .map(x -> x * 2) .toList();
Ce se întâmplă aici?
stream() creează o vedere funcțională asupra colecțieifilter selectează elementele doritemap transformă fiecare elementtoList() colectează rezultatul final
O interfață funcțională este o interfață care definește exact o metodă abstractă.
@FunctionalInterface public interface Operation { int apply(int a, int b); }
Această restricție permite compilatorului să știe ce metodă trebuie implementată de o expresie lambda, deoarece există doar o metodă de implementat.
defaultstatic
Avem următorul cod care definește cum putem folosi interfața funcțională Operation definită mai sus:
Operation add = new Operation() { @Override public int apply(int a, int b) { return a + b; } }; System.out.println(add.apply(2, 3));
O expresie lambda este o implementare inline a metodei unei interfețe funcționale.
Operation add = (a, b) -> a + b; System.out.println(add.apply(2, 3));
Este echivalent logic cu exemplul anterior.
O expresie lambda poate fi definită în două feluri:
(parametri) -> expresie (parametri) -> { bloc de cod }
Practic, vom avea mereu:
→) care specifică trecerea la corpul metodei,Exemple:
// o metodă cu un parametru care întoarce dublul lui x -> x * 2 // o metodă cu doi parametrii care întoarce suma lor (x, y) -> x + y // o metodă fără parametrii care întoarce "void" () -> System.out.println("Hello") // o metodă fără parametrii, mai complexă, care întoarce "void" () -> { System.out.println("Hello"); System.out.println("World"); } // o metodă cu un parametru, mai complexă, care întoarce un număr x -> { int y = x + 10; System.out.println("x is: " + x + " y is: " + y); return (x * 2) / y; }
Interfețele funcționale ne ajută practic să simulăm pointeri la funcții ca în limbajul C.
Totuși, pentru că Java este un limbaj OOP, are o restricție care împiedică folosirea sintaxei din C.
Tip de prim ordin (first-class) este un element care poate:
Exemple de tipuri first-class în Java:
int x = 10; String s = "hello"; Object o = new Object();
Însă, Java nu permite:
int add(int a, int b) { return a + b; } // NU este permis var f = add;
Nu poți scrie:
return add;
Nu poți spune:
void process(Function f) { }
În Java apar aceste restricții pentru că:
Expresiile lambda funcționează doar cu interfețe funcționale pentru că compilatorul știe ce metodă implementează, deoarece există doar o metodă de definit.
Interfața funcțională este un contract, iar lambda-ul este implementarea acestui contract.
Deci când scriem:
Operation add = (a, b) -> a + b; add.apply(2, 3);
Practic avem următorii pași:
Operation care definește o singură metodă abstractă.
Într-o lambda:
this se referă la clasa exterioarăthis propriuÎn schimb, într-o clasă anonimă:
this se referă la instanța anonimă
Java oferă, în pachetul java.util.function, un set de interfețe funcționale standard, gândite special pentru a fi folosite împreună cu lambda expressions și Stream API.
Aceste interfețe:
Predicate<T> modelează o condiție booleană aplicată asupra unui obiect de tip T.
@FunctionalInterface public interface Predicate<T> { boolean test(T t); }
TbooleanPredicate<Integer> isEven = x -> x % 2 == 0;
Utilizare:
isEven.test(10); // true
Function<T, R> modelează o transformare dintr-un tip T într-un tip R.
@FunctionalInterface public interface Function<T, R> { R apply(T t); }
TRFunction<String, Integer> length = s -> s.length();
Utilizare:
System.out.println(length.apply("ana")); // 3
Consumer<T> modelează o operație care primește un obiect T, nu returnează nimic, dar produce de obicei un side-effect.
@FunctionalInterface public interface Consumer<T> { void accept(T t); }
Consumer<String> printer = s -> System.out.println(s);
Utilizare:
printer.accept("Hello World"); // Hello World
Folosește Consumer doar:
int total = 0; void add(int x) { total += x; // modifică stare externă }
add(5); add(5); // rezultatul depinde de istoricul apelurilor, nu doar de parametri
int sum(int a, int b) { return a + b; }
Supplier<T> produce o valoare fără a primi nimic.
@FunctionalInterface public interface Supplier<T> { T get(); }
TSupplier<Double> random = () -> Math.random();
Utilizare:
System.out.println(random.get()); // un număr aleator
| Interfață | Metodă abstractă | Primește | Returnează | Descriere |
|---|---|---|---|---|
Predicate<T> | boolean test(T t) | T | boolean | Testează o condiție |
Function<T, R> | R apply(T t) | T | R | Transformă un tip în altul |
Consumer<T> | void accept(T t) | T | void | Consumă un obiect (side effects) |
Supplier<T> | T get() | nimic | T | Furnizează o valoare |
Un method reference este o formă prescurtată a unei expresii lambda, atunci când lambda-ul apelează direct o metodă existentă fără logică suplimentară.
Exemple de echivalență:
x -> System.out.println(x) System.out::println
x -> x.length() String::length
() -> new ArrayList<>() ArrayList::new
1. Static method
Integer::parseInt
2. Metodă a unei instanțe (pe un obiect)
System.out::println
3. Metodă a unei instanțe (pe un tip)
String::toUpperCase
4. Constructor
ArrayList::new
Un Stream reprezintă o vedere funcțională asupra unei surse de date (de obicei o colecție), care permite aplicarea unei succesiuni de transformări într-un mod declarativ.
Un stream:
Exemplu:
numbers.stream() .filter(x -> x % 2 == 0) .map(x -> x * 2) .toList();
Explicație:
numbers este sursastream() creează un streamfilter și map sunt transformăritoList() produce rezultatul finalEste esențial să nu confundăm aceste concepte pentru a nu avea erori în cod.
| Colecție | Stream |
|---|---|
| stochează date | procesează date |
| este concretă | este o abstracție |
| poate fi iterată de mai multe ori | se consumă o singură dată |
| permite modificări | este imutabil logic |
Exemplu greșit:
Stream<Integer> s = numbers.stream(); s.forEach(el -> System.out.println(el)); s.forEach(el -> System.out.println(el)); // EROARE
Un stream nu poate fi reutilizat.
Operațiile pe stream-uri sunt lazy, adică ele nu se execută imediat.
numbers.stream() .filter(x -> { System.out.println("filter: " + x); return x % 2 == 0; });
Codul de mai sus nu afișează nimic, deoarece nu există o operație terminală.
Execuția începe doar când apare o operație terminală (cum ar fi count()):
numbers.stream() .filter(x -> x % 2 == 0) .count();
Operațiile intermediare:
Exemple:
filter - Păstrează doar elementele care respectă o condiție.map - Transformă fiecare element într-un alt element.flatMap - Transformă fiecare element într-un stream și apoi aplatizează rezultatul.sorted - Sortează elementele stream-ului, natural sau cu un comparator.distinct - Elimină elementele duplicate (pe baza equals()).limit - Păstrează doar primele N elemente din stream.peek - Permite observarea elementelor fără a le modifica (debug/logging).Exemplu:
numbers.stream() .filter(x -> x > 0) .map(x -> x * 2);
Operațiile terminale:
Exemple:
Exemplu:
long count = numbers.stream() .filter(x -> x > 0) .count();
Unele operații pot opri execuția înainte de final.
Exemple:
anyMatch - Verifică dacă există cel puțin un element în stream care respectă o condiție (se oprește la prima potrivire).findFirst - Returnează primul element din stream, respectând ordinea (rezultat: Optional<T>)findAny – Returnează orice element din stream (fără garanție de ordine, optim pentru parallel streams).limit – Restrânge stream-ul la primele N elemente, oprind procesarea după atingerea limitei.Exemplu:
boolean hasEven = numbers.stream() .peek(x -> System.out.println(x)) .anyMatch(x -> x % 2 == 0);
În exemplul de mai sus, stream-ul printează elementele și se oprește la primul element par.
Stream API este construit direct peste interfețele funcționale din java.util.function, astfel că anumite metode din stream-uri acceptă doar anumite interfețe funcționale:
| Metodă Stream | Interfață funcțională |
|---|---|
| `filter` | `Predicate<T>` |
| `map` | `Function<T, R>` |
| `forEach` | `Consumer<T>` |
| `reduce` | `BinaryOperator<T>` |
| `anyMatch` | `Predicate<T>` |
Exemplu:
numbers.stream() .filter(x -> x % 2 == 0) // Predicate<Integer> .map(x -> x * 2) // Function<Integer, Integer> .forEach(System.out::println); // Consumer<Integer>
Stream-urile NU ar trebui să modifice stare externă, din motivele prezentate mai sus.
Exemplu greșit:
int sum = 0; numbers.stream() .forEach(x -> sum += x); // side effect
Exemplu corect:
int sum = numbers.stream().mapToInt(x -> x).sum();
Stream-urile nu funcționează pe etape separate. Fiecare element trece prin toate etapele separat de execuția celorlalte elemente.
Exemplu:
numbers.stream() .filter(x -> { System.out.println("filter " + x); return x % 2 == 0; }) .map(x -> { System.out.println("map " + x); return x * 2; }) .forEach(System.out::println);
Output:
filter 1 filter 2 map 2 4 filter 3 filter 4 map 4 8
Un parallel stream împarte procesarea pe mai multe thread-uri, folosind ForkJoinPool.
numbers.parallelStream() .map(x -> x * 2) .toList();
sau:
numbers.parallelStream() .map(x -> x * 2) .toList();
Pot fi utile când:
Nu este recomandat să folosim parallel streams când:
forEach cu side effectsExemplu problematic:
int sum = 0; numbers.parallelStream() .forEach(x -> sum += x); // RACE CONDITION
Exemplu corect:
int sum = numbers.parallelStream() .mapToInt(x -> x) .sum();
forEach nu garantează ordinea, în schimb forEachOrdered garantează ordinea în detrimentul vitezei de execuție.
Vi se dă scheletul pentru o aplicație ce simulează o situație mai mult sau mai puțin fictivă. Mai multe firme cooperează cu o bancă pentru a oferi salariul și beneficiile salariațiilor lor. Firmele au mai mulți salariați și mai multe proiecte în istoric. Este posibil ca unii salariați să fi schimbat firma la care lucrează, dar li se pastrează în istoricul personal proiectele la care au lucrat. Un salariat poate avea mai multe conturi bancare făcute prin firma la care lucrează.
Aveți de implementat:
Business, Bank si Employee - întoarceți tipurile corespunzatoare de date, dar asigurați-vă că sunt imutabile (hint: Collections.unmodifiable*)BankReport și BusinessReportPentru testare sunt folosite 10 teste, fiecare dintre acesta reprezentand 10 puncte.
Dezambiguizare: