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:
Aspectele bonus urmărite sunt:
Există mai multe paradigme de programare, fiecare definind un mod diferit de a structura și exprima logica unui program. Cele mai importante sunt programarea imperativă, care descrie execuția pas cu pas, programarea orientată pe obiecte, care modelează aplicația prin obiecte ce combină date și comportament, și programarea funcțională, care pune accent pe transformări de date, funcții pure și evitarea stării mutabile.
Majoritatea limbajelor moderne sunt multi-paradigmă. Java, de exemplu, combină stilul imperativ și orientat pe obiecte cu elemente de programare funcțională (lambda expressions, Stream API), permițând alegerea paradigmei potrivite în funcție de problemă.
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
and(…), or(…), negate(…).
Predicate<Integer> isPositive = x -> x > 0; Predicate<Integer> isEven = x -> x % 2 == 0; Predicate<Integer> positiveAndEven = isPositive.and(isEven); System.out.println(positiveAndEven.test(4)); // true System.out.println(positiveAndEven.test(3)); // false System.out.println(positiveAndEven.test(-2)); // false
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
Stream-urile generice (Stream<Integer>) folosesc:
Asta înseamnă că lucrul cu ele este mai lent și consumă mai multă memorie.
Exemplu folosire boxing:
int sum = numbers.stream() .map(x -> x) .reduce(0, Integer::sum);
Exemplu folosire primitive streams:
int sum = numbers.stream() .mapToInt(x -> x) .sum();
| Tip | Clasă |
|---|---|
| int | IntStream |
| long | LongStream |
| double | DoubleStream |
| Metodă | Ce face | Tip returnat | Observații importante |
|---|---|---|---|
mapToInt(…) | Transformă un Stream<T> într-un IntStream | IntStream | Evită autoboxing (Integer → int) |
sum() | Calculează suma elementelor din IntStream | int | Returnează 0 dacă stream-ul e gol |
average() | Calculează media aritmetică a elementelor | OptionalDouble | Folosește Optional pentru a evita 0/NaN |
min() | Găsește valoarea minimă din stream | OptionalInt | Stream-ul poate fi gol |
max() | Găsește valoarea maximă din stream | OptionalInt | Stream-ul poate fi gol |
Optional<T> este un container care poate:
Scopul lui este să evite folosirea lui null și apariția erorilor de tip NullPointerException.
Optional<String> opt = Optional.of("hello"); Optional<String> empty = Optional.empty();
Fără Optional avem următoarea situație:
String name = getName(); if (name != null) { System.out.println(name.length()); }
În schimb, cu Optional putem scrie codul de mai sus astfel:
Optional<String> name = getName(); name.ifPresent(n -> System.out.println(n.length()));
Optional, codul:
1. isPresent()
if (optional.isPresent()) { System.out.println(optional.get()); }
Ce face:
Optional conține o valoare.Problema:
get(), care poate arunca excepție dacă greșim logica
2. ifPresent(Consumer<T>)
optional.ifPresent(value -> System.out.println(value));
Ce face:
get()NullPointerExceptionCând se folosește:
3. orElse(T other)
String value = optional.orElse("default");
Ce face:
Optional“default”)Când se folosește:
Optional are valoare, deci este important să avem grijă la apelurile de metodă folosite ca valoare default:
optional.orElse(computeDefault()); // computeDefault() se execută oricum
4. orElseGet(…) (lazy)
String value = optional.orElseGet(() -> computeDefault());
Ce face:
OptionalSupplier-ulCând se folosește:
orElse, dacă includem ca valoare default un apel de metodă, aceasta este apelată doar dacă este necesar.
// recomandat optional.orElseGet(this::computeDefault);
orElse(T other).
5. orElseThrow()
String value = optional.orElseThrow();
Ce face:
NoSuchElementException dacă este emptyCând se folosește:
Echivalent logic cu:
if (optional.isEmpty()) { throw new NoSuchElementException(); } return optional.get();
Varianta cu excepție custom:
String value = optional.orElseThrow( () -> new IllegalStateException("Valoare lipsă") );
Optional poate fi cuplat cu Stream API pentru a crea secvențe de cod avansate:
Optional<Integer> firstEven = numbers.stream() .filter(x -> x % 2 == 0) .findFirst();
În exemplul de mai sus, findFirst() nu întoarce direct valoarea, ci un Optional<T>.
Nu folosiți get() direct. Dacă sunteți tentați să folosiți get(), probabil metoda potrivită este:
ifPresentorElseGetorElseThrow
Optional se folosește mai ales ca valoare de return, dar este important să știm că nu este:
Optional<T> este varianta generică, care funcționează cu orice tip obiect.
Caracteristici:
Integer, Double, String, etc.)Exemplu:
Optional<Integer> max = numbers.stream() .max(Integer::compareTo); max.ifPresent(System.out::println);
OptionalInt / OptionalLong și OptionalDouble sunt versiuni specializate pentru primitive.
Caracteristici:
intIntStreamExemplu:
OptionalInt max = numbers.stream() .mapToInt(x -> x) .max(); if (max.isPresent()) { System.out.println(max.getAsInt()); }
Folosește Optional<T> când:
Folosește OptionalInt când:
mapToIntUn 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: