Începând cu Java 8, putem implementa metode în cadrul interfețelor, unde, în mod clasic, avem doar metode neimplementate (abstracte). Mai precis, noi putem să implementăm două tipuri de metode: default (non-statice) și statice.
Metodele statice, în cadrul unei interfețe, se comportă identic cu metodele statice din clasă, în sensul că ele se apelează sub forma SomeInterface.metodaStatica()
și că acestea nu sunt moștenite nici de clasele care implementează interfața respectivă și nici de interfețele care extind respectiva interfață.
public interface SomeInterface { static void doStuff() { System.out.println("Doing something"); } } // apelul metodei statice SomeInterface.doStuff();
O metodă default este o metodă obișnuită, non-statică, publică în mod default și marcată prin keyword-ul default
, care se comportă ca o metodă moștenită (obișnuită și funcțională) în clasele ce implementează interfața ce conține metoda default respectivă.
Un motiv pentru care există metode default în Java este următorul: putem avea o interfață (o numim Animal
), care are mai multe metode, inclusiv shakeTail()
, și o clasă numită Human care implementează interfața respectivă. În acest caz, clasa Human
trebuie să implementeze metoda shakeTail()
, care nu este nicidecum relevantă, astfel am avea o implementare forțată a metodei shakeTail()
, în care am avea un body gol sau să aruncăm o excepție, încălcând astfel unul dintre principiile SOLID, mai precis Interface Segregation Principle
Exemplu:
interface Animal { default void shakeTail() { System.out.println("Humans have no tail"); } static void print() { System.out.println("This is an animal"); } void makeSound(); } class Human implements Animal { @Override public void makeSound() { System.out.println("Ahhhh"); } } public class Main { public static void main(String[] args) { Animal.print(); Animal human = new Human(); human.makeSound(); human.shakeTail(); } }
interface FirstInterface { default void doSomeAction() { System.out.println("FirstInterface action"); } void doJob(); } interface SecondInterface { default void doSomeAction() { System.out.println("SecondInterface action"); } void doStuff(); } class SomeClass implements FirstInterface, SecondInterface { // dacă nu am face override la doSomeAction, am avea eroare de compilare! @Override public void doSomeAction() { System.out.println("Action by SomeClass"); } @Override public void doJob() { System.out.println("Do the job"); } @Override public void doStuff() { System.out.println("Do the stuff"); } }
În Java 8, au fost introduse interfețele funcționale, care reprezintă interfețe ce conțin fix o metodă (neimplementată / abstractă). Aceste interfețe pot fi implementate sub forma de expresii lambda (despre care am vorbit în cadrul laboratorului de clase interne, introduse, de asemenea, în Java 8, pentru a reduce numărul de linii de cod, pentru a putea pasa funcții ca parametri la metode și pentru folosirea evaluării leneșe, despre care veți discuta la Paradigme de Programare, în semestrul următor), ele fiind folosite pentru a implementa funcții anonime în Java. Pentru a marca o interfață funcțională, este de recomandat să adăugam adnotarea @FunctionalInterface
, prin care se permite existența unei singure metode abstracte în cadrul unei interfețe funcționale.
@FunctionalInterface interface FunctionalInterface { int sum(int a, int b); } class Main { public static void main(String[] args) { FunctionalInterface sumOp = (a, b) -> a + b; System.out.println(sumOp.sum(1, 2)); } }
În cadrul pachetului java.util.function, există interfețe funcționale predefinite, care pot fi folosite mai ales în cadrul colecțiilor (streams).
Consumer<T> - reprezintă o funcție care primește un parametru de tip T și nu întoarce nimic (o funcție void).
Exemplu:
public void whenNamesPresentConsumeAll(){ Consumer<String> printConsumer = t -> System.out.println(t); Stream<String> cities = Stream.of("Sydney", "Dhaka", "New York", "London"); cities.forEach(printConsumer); }
Interfata Consumer<T> expune si o metoda andThen(Consume<T>) care poate inlantui alte operatii de tipul Consumer.
Exemplu:
public void whenNamesPresentUseBothConsumer(){ List<String> cities = Arrays.asList("Sydney", "Dhaka", "New York", "London"); Consumer<List<String>> upperCaseConsumer = list -> { for(int i=0; i< list.size(); i++){ list.set(i, list.get(i).toUpperCase()); } }; Consumer<List<String>> printConsumer = list -> list.stream().forEach(System.out::println); upperCaseConsumer.andThen(printConsumer).accept(cities); }
Predicate<T> - reprezintă o funcție booleană care primește un singur parametru de tip T. Ea este folosita in general impreuna cu functiile de filter pe stream-uri.
public void testPredicate(){ List<String> names = Arrays.asList("John", "Smith", "Samueal", "Catley", "Sie"); Predicate<String> nameStartsWithS = str -> str.startsWith("S"); names.stream().filter(nameStartsWithS).forEach(System.out::println); }
Interfata Predicate<T> expune si metodele and(Predicate<T>), not(Predicate<T>), or(Predicate<T>) care pot inlantui alte operatii de tipul Predicate.
Exemplu:
public void testPredicateAndComposition(){ List<String> names = Arrays.asList("John", "Smith", "Samueal", "Catley", "Sie"); Predicate<String> startPredicate = str -> str.startsWith("S"); Predicate<String> lengthPredicate = str -> str.length() >= 5; names.stream().filter(startPredicate.and(lengthPredicate)).forEach(System.out::println); }
Function<T, R> - reprezintă o funcție care primește un parametru de tip T și întoarce un rezultat de tip R. Ea este folosita in general impreuna cu functiile de map pe stream-uri.
public void testFunctions(){ List<String> names = Arrays.asList("Smith", "Gourav", "Heather", "John", "Catania"); Function<String, Integer> nameMappingFunction = String::length; List<Integer> nameLength = names.stream().map(nameMappingFunction).collect(Collectors.toList()); System.out.println(nameLength); }
Interfata Function<T, R> expune si metode care pot inlantui alte operatii de tipul Function.
Supplier<T> - reprezintă o funcție care nu primește niciun parametru și întoarce un rezultat de tip T, prin funcția get()
si este folosita ca target pentru o functie lambda.
public void supplier(){ Supplier<Double> doubleSupplier1 = () -> Math.random(); DoubleSupplier doubleSupplier2 = Math::random; System.out.println(doubleSupplier1.get()); System.out.println(doubleSupplier2.getAsDouble()); }
De asemenea, interfața Comparator este o interfață funcțională, având o singură metodă abstractă - compare.
Stream-urile au fost introduse în Java 8, pentru a permite programatorului să efectueze operații de tipul filter, map și reduce pe colecții într-un mod elegant și eficient.
Stream-urile pot fi obtinute prin mai multe modalitati, in special de la liste si set-uri, care expun metodele stream()
si parallelStream()
. Diferenta consta in faptul ca cea dintai face operatiile secvential in timp ce a doua le face in paralel pe un mediu de procesare cu mai multe core-uri.
Exemple:
Arrays.asList("a1", "a2", "a3") .stream() .findFirst() .ifPresent(System.out::println); // a1
Stream.of("a1", "a2", "a3") .findFirst() .ifPresent(System.out::println); // a1
IntStream.range(1, 4) .forEach(System.out::println);
Arrays.stream(new int[] {1, 2, 3}) .map(n -> 2 * n + 1) .average() .ifPresent(System.out::println); // 5.0
Provenite din Programarea functionala, functiile de map()
, filter()
si reduce
au fost importate in Java ca principal mijloc de lucru cu Stream-uri. Ele ajuta la reducerea considerabila a codului scris, dar pot, in acelasi timp, sa ingreuneze intelegerea acestuia, cu atat mai mult pentru programatorii aflati la inceput de cariera.
Permite conversia datelor dintr-un Stream prin schimbare a tipului sau modificare a valorii.
String[] myArray = new String[]{"bob", "alice", "paul", "ellie"}; Stream<String> myStream = Arrays.stream(myArray); Stream<String> resultStream1 = myStream.map(s -> s.toUpperCase()); // ["Bob", "Alice", "Paul", "Ellie"] Stream<Integer> resultStream2 = myStream.map(s -> s.length()); // [3, 5, 4, 5] class Student{ String name; Integer age; Student(String name, Integer age) { this.name = name; this.age = age; } } Stream<Student> resultStream3 = myStream1.map(s -> new Student(s, s.length())); // [Student: {"Bob", 3}, Student: {"Alice", 5}, Student: {"Paul", 4}, Student: {"Ellie", 5}]
Exemple de conversie inversa (de la Stream la un tip de date):
String[] myNewArray = resultStream1.toArray(String[]::new); List<String> myNewArray2 = (ArrayList<String>)resultStream1.collect(Collectors.toList()); Set<Integer> myNewSet = (Set<Integer>) resultStream2.collect(Collectors.toSet());
Asa cum spune si numele, ajuta la filtrarea elementelor unui Stream, mai precis la eliminarea elementelor nedorite din acesta. Precum functia de map()
, asteapta ca argument o expresie lambda
, doar ca, de data aceasta, expresia trebuie sa returneze un boolean ce va determina daca o valoare ramane in Stream.
ArrayList<String> myArray = (ArrayList<String>) Arrays.asList("dog", "cat", "monkey", "elephant", "rat", "lion", "zebra"); String[] myNewArray = Arrays.stream((String[])myArray.toArray()) .filter(x -> x.length() > 4) .toArray(String[]::new);
Metodele de tip reduce
sunt metode ce obtin un rezultat in urma unei operatii pentru intregul set de date. De aceea, le vom numi si metode terminale
, deoarece setul de operatii functionale pe care il vom aplica asupra Stream-ului se va termina aproape intotdeauna cu o operatie de tip reduce
.
Deja ati intalnit operatia toArray()
anterior, care este o operatie de tip reduce
. Alte operatii similare ar fi sum
, average
si count
.
Functia in sine de reduce()
, spre deosebire de map
si filter
, accepta doua argumente: un element identitate (sau acumulator) si o expresie lambda.
String[] myArray = { "this", "is", "a", "sentence" }; String result = Arrays.stream(myArray) .reduce("", (a,b) -> a + b);
În Java 10, var
a fost întrodus pentru a face munca unui programator mai lejeră și acesta poate fi folosit doar în interiorul blocurilor de cod (nu poate fi folosit în declararea câmpurilor unei clase sau la semnătura unei metode).
var
poate fi folosit doar când o variabilă este declarată și i se atribuie în același timp o valoare, prin care se deduce tipul variabilei (se aplică doar dacă tipul variabilei se identifică în timpul compilării).
Exemplu:
var n = 10; // se deduce ca tipul este int var list = new ArrayList(); // se deduce ca tipul este ArrayList var p; // aceasta declaratie este eronata, genereaza eroare de compilare, // deoarece nu se poate deduce tipul variabilei (trebuie neaparata atribuita o valoare la declaratie)
Vi se da scheletul pentru o aplicatie ce simuleaza o situatie mai mult sau mai putin fictiva. Mai multe firme coopereaza cu o banca pentru a oferi salariul si beneficiile salariatilor lor. Firmele au mai multi salariati si mai multe proiecte in istoric. Este posibil ca unii salariati sa fi schimbat firma la care lucreaza, dar li se pastreaza in istoricul personal proiectele la care au lucrat. Un salariat poate avea mai multe conturi bancare facute prin firma la care lucreaza.
Aveti de implementat:
Business
, Bank
si Employee
- returnati tipurile corespunzatoare de date, dar asigurati-va ca sunt imutabile (hint: Collections.unmodifiable*
) (0.2p / metoda)BankReport
(1p / metoda)BusinessReport
(0.5p / metoda) Dezambiguizare: