PRA03.rst

Pracownia Programowania

Uwaga! cały kod do Zadania 7 włącznie znajduje się na gałęzi StreamsAndCollectionsStart w repozytorium https://github.com/WitMar/PRA2018-2019 . Kod końcowy w gałęzi StreamsAndCollectionsEnd.

Jeżeli nie widzisz odpowiednich gałęzi na GitHubie wykonaj Ctr+T, a jak to nie pomoże to wybierz z menu VCS->Git->Fetch.

Fluent API

Termin Interfejs Fluent został wymyślony przez Martina Fowlera i Erica Evansa. Fluent API oznacza zbudowanie API w taki sposób, aby spełniało następujące kryteria:

* Programista może bardzo łatwo zrozumieć interfejs API.
* Interfejs API może wykonać szereg działań na raz. W Javie możemy to zrobić za pomocą szeregu wywołań metod (łańcuch metod).
* Nazwa każdej metody powinna być związana z domeną w której operuje.
* Interfejs API powinien być wystarczająco sugestywny, aby wskazać użytkownikom interfejsu API, co robić dalej i jakie możliwe operacje mogą podjąć użytkownicy w danym momencie.

The Fluent Interface builder należy używać, gdy konstruktor ma więcej niż cztery lub pięć parametrów, tworzymy klasę konstruktora, która pomaga w tworzeniu obiektów.

Główną ideą jest by zamiast:

Person(String name, Title title) {
        this.name = name;
        this.title = title;
}

Person adam = new Person("Adam Z.", Title.PROF);

Używać bardziej łatwej w analizie fasady otaczającej konstruktor w formie tzw. buildera:

public class PersonBuilderBuilder implements IPersonBuilder {
  Title title;
  String name;

  public Person build() {
      Person person = new Person(this.name, this.title);
      return person;
  }

  @Override
  public IPersonBuilder withName(String name) {
      this.name = name;
      return this;
  }

  @Override
  public IPersonBuilder withTitle(Title title) {
      this.title = title;
      return this;
  }
}

  PersonBuilderBuilder personBuilder = new PersonBuilderBuilder();
  Person person = personBuilder.withName("Marcin Witkowski")
          .withTitle(Title.DR)
          .build();

Co znacznie ułatwia odczytanie kodu.

W powyższym przykładzie używamy również interfejsów, które w ogólności nie są konieczne, popatrz także przykład:

Ten sam pomysł można zastosować nie tylko w przypadku budowy nowego obiektu, ale także do zarządzania obiektami i ich metodami.

Najważniejszym elementem jest tutaj to, że implementacja fluent API wymaga przekazania tego samego obiektu jako wyniku funkcji (return this).

Ponownie w przykładzie używamy interfejsu, który w ogólności nie jest konieczny:

public class Person implements IPerson {
  List<Person> friends = new ArrayList<>();
  String name;
  Enum title;

  @Override
  public IPerson addFriend(Person friend) {
      this.friends.add(friend);
      return this;
  }
}

Aby dodać znajomego dla osoby, wykonujemy metodę addFriend(), która zwraca obiekt Person, który pozwala nam korzystać łańcuchowo z innych metod (podobnie działają potoki w systemach operacyjnych).

adam.addFriend(Ben).addFriend(Barrack).addFriend(John);

Task 1: Builder

Przejdź do klasy MainFluentApi i stwórz obiekt klasy Person korzystając z Buildera. Sprawdź czy możesz stworzyć obiekt korzystając z konstruktora klasy poprzez operator new Person().

Task 2: Friends

Stwórz dwie inne osoby i dodaj je jako przyjaciół pierwszej z osób.

Task 3: Hello

Zaimplementuj metodę sayHelloToFriends w klasie Person tak by wypisywała ona na ekran "Hello <person name>". Sprawdź czy implementacja działa.

Function

Function to specjalny interfejs w javie do definiowania funkcji. Posiada on metody apply()

Function<Integer, Double> half = a -> a / 2.0;
  // apply the function to get the result
System.out.println(half.apply(10));

addThen()

Function<Integer, Double> half = a -> a / 2.0;
// Now treble the output of halffunction
half = half.andThen(a -> 3 * a);
// apply the function to get the result
System.out.println(half.apply(10));

identity() - zwraca funkcję identycznościową (zwracającą na wyjście to co podamy na wejściu).

Function<Integer, Double> half = a -> a / 2.0;
// Now treble the output of halffunction
half = half.andThen(Function.identity());
// apply the function to get the result
System.out.println(half.apply(2));

Jak można zauważyć, możesz używać wyrażeń Lamba ** o notacji **x-> x w celu zdefiniowania funkcji w Javie. Lewa strona strzałki oznacza argumenty (w tym przypadku pojedynczy argument), a prawa strona oznacza funkcję, która może skłądać się z jednej lub większej liczby instrukcji (w tym drugim przypadku należy użyć {}).

Task 4: Process

Zaimplementuj funkcje processFriends() w ten sposób by czyściła listę przyjaciół.

Task 5: Best friend

Zaimplementuj funkcję wyboru best friend tak by wybierała jako najlepszego przyjaciela pierwszy element listy przyjaciół. Sprawdź co stanie się gdy lista będzie pusta.

Kolekcje

Kolekcje w Java:

col

map

Przykłady:

List<Integer> list = new ArrayList<>();
Set<Integer> list = new HashSet<>();

Strumienie

Tradycyjnie w Javie do przechodzenia po kolekcjach korzystamy z iteratorów jawnie .. class:: highlight

List <String> names = new ArrayList <>();
for (Student student: students) {
    if (student.getName ().startsWith("A")) {
        names.add (student.getName ());
    }
}

Wykorzystując strumienie korzystamy z tzw. wewnętrznych iteracji, to znaczy logika iteracji po elementach jest dla nasz ukryta a skupiamy się na ich przetwarzaniu.

List <String> namesNewJava = students.stream()
              .map(student -> student.getName())
              .filter (name-> name.startsWith("A"))
              .collect (Collectors.toList());

Operacje na strumieniach

Operacja na strumieniach dzielimy na pośrednie i końcowe (terminalne). Operacje pośrednie zwracają jako wynik działania strumień. W powyższym przykładzie takimi operacjami są map i filter. Operacje terminalne kończą przetwarzanie, zwykle agregują one wyniki, zliczają wartości, albo też nic nie wykonują (np. operacja foreach może być terminalna). Powyżej przykładem takiej operacji jest collect.

Dla operacji numerycznych stworzono specjalne strumienie IntStream, DoubleStream, and LongStream.

IntStream.rangeClosed(1, 10).forEach(num -> System.out.print(num));
// ->12345678910
IntStream.range(1, 10).forEach(num -> System.out.print(num));
// ->123456789

Budowa strumieni

Stream stałych i elementów tablicy:

Stream.of("This", "is", "Java8", "Stream").forEach(System.out::println);

String[] stringArray = new String[]{"Streams", "can", "be", "created", "from", "arrays"};
stringArray.forEach(System.out::println);

Elementy strumieni

Filter

Wybiera elementy ze strumienia względem danego warunku

students.stream()
                .filter(student -> student.getScore() >= 60)
                .collect(Collectors.toList());

Map

Zmienia przetwarzany element na coś innego, czyli pobiera element jednego typu i zwraca element innego typu. W praktyce najczęściej służy do wyciągnięcia jakiś atrybutów z obiektów.

students.stream()
        .map(Student::getName)
        .forEach(System.out::println);

The Student::getName jest skrótowym odwołaniem się do metody z klasy. Czyli na obiekcie typu Student, który przyjdzie ze streama wywołujemy metodę getName().

Równoważnie możemy zapisać taki strumień jako:

students.stream()
        .map(student -> student.getName())
        .forEach(System.out::println);

Distinct

Usuwa powtórzenia

students.stream()
                .map(Student::getName)
                .distinct()
                .collect(Collectors.toList());

Limit

Ogranicza liczbę elementów w strumieniu do podanej liczby.

Sorted

Sortuje elementy w naturalnym dla nich porządku.

students.stream()
                .map(Student::getName)
                .sorted()
                .collect(Collectors.toList());

Z pomocą klasy Comparator możemy definiować sortowania różnego typu

//Sorting names if the Students in descending order
students.stream()
                .map(Student::getName)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
//Sorting names if the Students in descending order
students.stream()
                .map(Student::getName)
                .sorted(Comparator.reverseOrder())
                .collect(Collectors.toList());
//Sorting students by First Name and Last Name both
students.stream()
                .sorted(Comparator.comparing(Student::getFirstName).
                                thenComparing(Student::getLastName))
                .map(Student::getName)
                .collect(Collectors.toList());
//Sorting students by First Name Descending and Last Name Ascending
students.stream()
                .sorted(Comparator.comparing(Student::getFirstName)
                                .reversed()
                                .thenComparing(Student::getLastName))
                .map(Student::getName)
                .collect(Collectors.toList());

FlatMap

Działa podobnie jak mapa, tylko jako wynik zwraca strumień przetworzonych elementów.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
List<List<Integer>> mapped =
                                numbers.stream()
                                .map(number -> Arrays.asList(number -1, number, number +1))
                                .collect(Collectors.toList());

System.out.println(mapped); //:> [[0, 1, 2], [1, 2, 3], [2, 3, 4], [3, 4, 5]]

List<Integer> flattened =
                                numbers.stream()
                                .flatMap(number -> Arrays.asList(number -1, number, number +1).stream())
                                .collect(Collectors.toList());

System.out.println(flattened);  //:> [0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5]

Elementy Terminalne

Match

Match stosujemy gdy interesuje nas występowanie w strumieniu elementu o danych własnościach.

//Check if at least one student has got distinction
Boolean hasStudentWithDistinction = students.stream()
                .anyMatch(student -> student.getScore() > 80);

//Check if All of the students have distinction
Boolean hasAllStudentsWithDistinction = students.stream()
                .allMatch(student -> student.getScore() > 80);

//Return true if None of the students are over distinction
Boolean hasAllStudentsBelowDistinction = students.stream()
                .noneMatch(student -> student.getScore() > 80);

Find

Pozwala znajdować obiekt w strumieniu o danych włanościach, możemy wybrać dowolny dopasowany obiekt lub pierwszy dopasowany.

//Returns any student that matches to the given condition
students.stream().filter(student -> student.getAge() > 20)
                        .findAny();
//Returns first student that matches to the given condition
students.stream().filter(student -> student.getAge() > 20)
                        .findFirst();

Reduce

Pozwala zredukować strumień danych do pojedynczej wartości.

//Summing without passing an identity
Optional<integer> sum = numbers.stream()
                .reduce((x, y) -> x + y);
//Product without passing an identity
Optional<integer> product = numbers.stream()
                .reduce((x, y) -> x * y);

Collect

Zbiera elementy strumienia w listę.

students.stream()
                .filter(student -> student.getScore() >= 60)
                .collect(Collectors.toList());

Ważne: Collect zwraca nowy obiekt! Nową listę, mapę, zbiór etc. ale wartości są te same co w przetwarzanej kolekcji.

RemoveIf

Usuwanie z kolekcji elementów spełniających warunek.

students.removeIf(student -> student.getScore() >= 60)

Task 6: Streams

Zaimplementuj korzystając ze strumieni brakujące metody w klasie Tasks. Spradź uruchamiając TasksTest czy dobrze wykonałeś zadanie.

Local variables

To się nie skompiluje:

int i = 0;
IntStream.range(1, 10).forEach(number -> {
    if (number < i) {
        System.out.println("Smaller");
        i++;
    }
});

Podstawowym powodem, dla którego kompilacja się nie powiedzie, jest przechwycenie przez lambda wyrażenie wartości i, co oznacza wykonanie jej kopii w momencie inicjalizacji. Wymuszenie, by zmienna była ostateczna, pozwala uniknąć wrażenia, że ​​zwiększenie i wewnątrz lambda mogłoby faktycznie zmodyfikować parametr metody i.

Podczas gdy to się skompiluje:

private int i =0;

public void method() {
   IntStream.range(1, 10).forEach(number -> {
        if (number < i) {
            System.out.println("Smaller");
            i++;
        }
    });
 }

Mówiąc najprościej, chodzi o to, gdzie przechowywane są zmienne składowe. Zmienne lokalne znajdują się na stosie, ale zmienne składowe są na stercie. Ponieważ mamy do czynienia z pamięcią sterty, kompilator może zagwarantować, że lambda będzie miała dostęp do najnowszej wartości zmiennej.

Z drugiej strony zmiana wartości zmiennych w strumieniu może powodować problemy podczas ich równoległego wykonywania:

IntStream stream = IntStream.range(1, 100);
stream.parallel().forEach(number -> {
   if (number > i) {
       System.out.println(i + " smaller than + number);
       i+=2;
   }
 });
Eclipse collections

Kolekcje służą do przechowywania elementów w określonej kolejności, która pozwala na sprawne znalezienie elementów, wydajne wykorzystanie pamięci lub zapisywania elementów w określonej kolejności. Przykładami są Zbiory (Set), mapy (Map), kolejki (Queue) itp.

Implementacje kolekcji z biblioteki Eclipse Collections zapewniają efektywną pamięciowo implementację zbiorów i map, a także kolekcji podstawowych kolekcje.

Eclipse Collections powstało jako struktura kolekcji o nazwie Caramel wykorzystywana wewnętrznie przez bank Goldman Sachs w 2004 roku. po paru latach projekt został przeniesiony do open source pod nazwą GS Collections, a następnie do Eclipse Foundation, pod ostateczną nazwą Eclipse Collections w 2015 roku. Projekt jest w pełni otwarty i darmowy.

Zależność Mavena:

<dependency>
    <groupId>org.eclipse.collections</groupId>
    <artifactId>eclipse-collections-api</artifactId>
    <version>9.2.0</version>
</dependency>

<dependency>
    <groupId>org.eclipse.collections</groupId>
    <artifactId>eclipse-collections</artifactId>
    <version>9.2.0</version>
</dependency>

Struktury zaimplementowane w kolekcjach Eclipse są zwykle szybsze i zajmują mniej pamięci niż te z java.util. W kolekcjach Eclipse można ponadto znaleźć dodatkowe struktury, których nie można znaleźć w java.util. Jednym z nich jest MultiMapa.

MultiMapa to mapa, do której każdemu kluczowi można przypisać więcej niż jedną wartość. Wartości na mapie są następnie zapisywane jako lista wartości połączona z kluczem.

Przykład:

FastListMultimap<String, String> citiesToPeople = FastListMultimap.newMultimap();

citiesToPeople.put("Poznan", "Nowak");
citiesToPeople.put("Poznan", "Kowalski");

citiesToPeople.get("Poznan")
    .forEach(name -> System.out.println(name));

Więcej przykładów:

Task 7: MultiMap

Zaimplementuj funkcję partitioningAdults w klasie MainEclipseCollection tak by przechodziła testy. Użyj wyszukiwarki google do znalezienia informacji o tym jak należy wykonać collect by otrzymać element z biblioteki EclipseCollection.

*

Used materials from: