Java features

Obiective

  • înțelegerea conceptelor de expresii lambda și de streams
  • familiarizarea cu metode default și cu metode statice în interfețe
  • utilizarea de structuri sintactice introduse începând cu Java 8 (var)

Metode statice și metode default in interfețe

Î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.

Metode 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();

Metode default

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();
    }
}

Având în vedere că moștenirea multiplă nu este suportată în Java, mai precis extinderea a două clase în același timp, această problemă se propagă și la interfețe, în cazul metodelor default. Această problemă apare dacă o clasa implementează două interfețe, fiecare având o metodă default cu aceeași semnătură, în acest caz apărând eroare de compilare, care se rezolvă dacă respectiva clasă are propria sa implementare pentru metodele default din acele interfețe. Pentru o mai bună înțelegere, urmăriți exemplul de mai jos.

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");
    }
}

Interfețe funcționale, funcții și expresii lambda

Î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

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);
}

Exista si o interfata BiConsumer<T, U> care primeste doua argumente in loc de unul singur, dar are aceeasi functionalitate.

Predicate

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);
}

Exista si o interfata BiPredicate<T, U> care primeste doua argumente in loc de unul singur, dar are aceeasi functionalitate.

Function

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.

Exista si o interfata BiFunction<T, U, R> care primeste doua argumente in loc de unul singur, dar are aceeasi functionalitate.

Supplier

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.

Streams

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

Map, Filter, Reduce

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.

Map

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());

Filter

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);

Reduce

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);

var

Î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)

Exerciții

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:

  • TODO-urile din clasele Business, Bank si Employee - returnati tipurile corespunzatoare de date, dar asigurati-va ca sunt imutabile (hint: Collections.unmodifiable*) (0.2p / metoda)
  • metodele statice din clasa BankReport (1p / metoda)
  • [bonus] metodele statice din clasa BusinessReport (0.5p / metoda)

Dezambiguizare:

  • Customer = Employee of the Business
  • Business = a client of the Bank
  • Customers of the Bank = all the Employees that work for the Businesses that are clients of the Bank

Resurse

poo-ca-cd/laboratoare/java-features.txt · Last modified: 2021/01/20 11:38 by adriana.draghici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0