Dlaczego musisz już uaktualnić do wersji Java 8
Opublikowany: 2022-03-11Najnowsza wersja platformy Java, Java 8, została wydana ponad rok temu. Wiele firm i programistów nadal pracuje z poprzednimi wersjami, co jest zrozumiałe, ponieważ istnieje wiele problemów z migracją z jednej wersji platformy na drugą. Mimo to wielu programistów wciąż uruchamia nowe aplikacje ze starymi wersjami Javy. Jest bardzo niewiele dobrych powodów, aby to zrobić, ponieważ Java 8 wprowadziła kilka ważnych ulepszeń do języka.
W Javie 8 pojawiło się wiele nowych funkcji. Pokażę Ci garść tych najbardziej przydatnych i interesujących:
- Wyrażenia lambda
- Stream API do pracy z kolekcjami
- Asynchroniczne łączenie zadań w łańcuchy z
CompletableFuture
- Zupełnie nowy interfejs API czasu
Wyrażenia lambda
Lambda to blok kodu, do którego można się odwoływać i przekazywać do innego fragmentu kodu w celu wykonania w przyszłości raz lub więcej razy. Na przykład funkcje anonimowe w innych językach to lambdy. Podobnie jak funkcje, lambdy mogą być przekazywane jako argumenty w momencie ich wykonania, modyfikując ich wyniki. Java 8 wprowadziła wyrażenia lambda , które oferują prostą składnię do tworzenia i używania lambd.
Zobaczmy przykład, jak może to ulepszyć nasz kod. Tutaj mamy prosty komparator, który porównuje dwie wartości Integer
według ich modulo 2:
class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }
Instancja tej klasy może zostać wywołana w przyszłości w kodzie, w którym ten komparator jest potrzebny, na przykład:
... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...
Nowa składnia lambda pozwala nam to zrobić prościej. Oto proste wyrażenie lambda, które robi to samo, co metoda compare
z BinaryComparator
:
(Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Struktura ma wiele podobieństw do funkcji. W nawiasach ustawiamy listę argumentów. Składnia ->
pokazuje, że jest to lambda. W prawej części tego wyrażenia ustawiamy zachowanie naszej lambdy.
Teraz możemy poprawić nasz poprzedni przykład:
... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...
Tym obiektem możemy zdefiniować zmienną. Zobaczmy jak to wygląda:
Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;
Teraz możemy ponownie wykorzystać tę funkcjonalność, w ten sposób:
... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...
Zauważ, że w tych przykładach lambda jest przekazywana do metody sort()
w taki sam sposób, jak instancja BinaryComparator
jest przekazywana we wcześniejszym przykładzie. Skąd maszyna JVM wie, jak poprawnie zinterpretować lambdę?
Aby umożliwić funkcjom przyjmowanie lambd jako argumentów, Java 8 wprowadza nową koncepcję: interfejs funkcjonalny . Interfejs funkcjonalny to interfejs, który ma tylko jedną metodę abstrakcyjną. W rzeczywistości Java 8 traktuje wyrażenia lambda jako specjalną implementację funkcjonalnego interfejsu. Oznacza to, że aby otrzymać lambdę jako argument metody, zadeklarowany typ tego argumentu musi być tylko interfejsem funkcjonalnym.
Kiedy deklarujemy funkcjonalny interfejs, możemy dodać notację @FunctionalInterface
, aby pokazać programistom, co to jest:
@FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }
Teraz możemy wywołać metodę sendDTO
, przekazując różne lambdy, aby uzyskać inne zachowanie, na przykład:
sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));
Odniesienia do metod
Argumenty lambda pozwalają nam modyfikować zachowanie funkcji lub metody. Jak widać w ostatnim przykładzie, czasami lambda służy tylko do wywołania innej metody ( sendToAndroid
lub sendToIos
). W tym szczególnym przypadku Java 8 wprowadza wygodny skrót: odwołania do metod . Ta skrócona składnia reprezentuje lambdę, która wywołuje metodę i ma postać objectName::methodName
. Dzięki temu poprzedni przykład będzie jeszcze bardziej zwięzły i czytelny:
sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);
W tym przypadku metody sendToAndroid
i sendToIos
są zaimplementowane w this
klasie. Możemy również odwoływać się do metod innego obiektu lub klasy.
Strumieniowy interfejs API
Java 8 zapewnia nowe możliwości pracy z Collections
w postaci zupełnie nowego interfejsu Stream API. Ta nowa funkcjonalność jest dostarczana przez pakiet java.util.stream
i ma na celu umożliwienie bardziej funkcjonalnego podejścia do programowania przy użyciu kolekcji. Jak zobaczymy, jest to możliwe w dużej mierze dzięki nowej składni lambda, którą właśnie omówiliśmy.
Interfejs API Stream oferuje łatwe filtrowanie, liczenie i mapowanie kolekcji, a także różne sposoby uzyskiwania z nich wycinków i podzbiorów informacji. Dzięki składni w stylu funkcjonalnym interfejs API Stream umożliwia krótszy i bardziej elegancki kod do pracy z kolekcjami.
Zacznijmy od krótkiego przykładu. Użyjemy tego modelu danych we wszystkich przykładach:
class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }
Wyobraźmy sobie, że musimy wydrukować wszystkich autorów ze zbioru books
, którzy napisali książkę po 2005 roku. Jak zrobilibyśmy to w Javie 7?
for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }
A jak zrobimy to w Javie 8?
books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element
To tylko jedno wyrażenie! Wywołanie metody stream()
na dowolnej Collection
zwraca obiekt Stream
zawierający wszystkie elementy tej kolekcji. Można to manipulować za pomocą różnych modyfikatorów z interfejsu API Stream, takich jak filter()
i map()
. Każdy modyfikator zwraca nowy obiekt Stream
z wynikami modyfikacji, którymi można dalej manipulować. Metoda .forEach()
pozwala nam wykonać jakąś akcję dla każdej instancji wynikowego strumienia.
Ten przykład pokazuje również bliski związek między programowaniem funkcjonalnym a wyrażeniami lambda. Zwróć uwagę, że argument przekazywany do każdej metody w strumieniu jest niestandardową lambdą lub odwołaniem do metody. Z technicznego punktu widzenia każdy modyfikator może otrzymać dowolny interfejs funkcjonalny, jak opisano w poprzedniej sekcji.
Stream API pomaga programistom spojrzeć na kolekcje Java z nowej perspektywy. Wyobraź sobie teraz, że musimy uzyskać Map
dostępnych języków w każdym kraju. Jak zostałoby to zaimplementowane w Javie 7?
Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }
W Javie 8 jest trochę schludniej:
import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
Metoda collect()
pozwala nam zbierać wyniki strumienia na różne sposoby. Tutaj widzimy, że najpierw grupuje się według kraju, a następnie mapuje każdą grupę według języka. ( groupingBy()
i toSet()
to metody statyczne z klasy Collectors
.)

Istnieje wiele innych możliwości Stream API. Pełna dokumentacja znajduje się tutaj. Polecam czytać dalej, aby lepiej zrozumieć wszystkie potężne narzędzia, które ten pakiet ma do zaoferowania.
Asynchroniczne łączenie zadań w łańcuchy z CompletableFuture
W pakiecie java.util.concurrent
Java 7 znajduje się interfejs Future<T>
, który pozwala nam w przyszłości uzyskać status lub wynik jakiegoś asynchronicznego zadania. Aby skorzystać z tej funkcjonalności, musimy:
- Utwórz
ExecutorService
, który zarządza wykonywaniem zadań asynchronicznych i może generowaćFuture
obiekty w celu śledzenia ich postępu. - Utwórz asynchronicznie
Runnable
zadanie. - Uruchom zadanie w
ExecutorService
, który zapewniFuture
dając dostęp do statusu lub wyników.
Aby wykorzystać wyniki zadania asynchronicznego, należy z zewnątrz monitorować jego postęp za pomocą metod interfejsu Future
, a gdy jest gotowy, jawnie pobrać wyniki i wykonać na nich dalsze akcje. Może to być dość skomplikowane do wdrożenia bez błędów, zwłaszcza w aplikacjach z dużą liczbą jednoczesnych zadań.
Jednak w Javie 8 koncepcja Future
jest rozwijana dalej, z interfejsem CompletableFuture<T>
, który umożliwia tworzenie i wykonywanie łańcuchów zadań asynchronicznych. Jest to potężny mechanizm do tworzenia aplikacji asynchronicznych w Javie 8, ponieważ pozwala nam automatycznie przetwarzać wyniki każdego zadania po zakończeniu.
Zobaczmy przykład:
import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);
Metoda CompletableFuture.supplyAsync
tworzy nowe zadanie asynchroniczne w domyślnym Executor
(zazwyczaj ForkJoinPool
). Po zakończeniu zadania jego wyniki zostaną automatycznie dostarczone jako argumenty funkcji this::getLinks
, która jest również uruchamiana w nowym zadaniu asynchronicznym. Na koniec wyniki tego drugiego etapu są automatycznie drukowane do System.out
. thenApply()
i thenAccept()
to tylko dwie z kilku przydatnych metod, które pomagają tworzyć współbieżne zadania bez ręcznego korzystania z Executors
.
CompletableFuture
ułatwia zarządzanie sekwencjonowaniem złożonych operacji asynchronicznych. Powiedzmy, że musimy stworzyć wieloetapową operację matematyczną z trzema zadaniami. Zadanie 1 i zadanie 2 wykorzystują różne algorytmy, aby znaleźć wynik dla pierwszego kroku i wiemy, że tylko jeden z nich zadziała, podczas gdy drugi zawiedzie. Jednak to, które z nich działa, zależy od danych wejściowych, których nie znamy z wyprzedzeniem. Wynik z tych zadań należy zsumować z wynikiem zadania 3 . Dlatego musimy znaleźć wynik zadania 1 lub zadania 2 oraz wynik zadania 3 . Aby to osiągnąć, możemy napisać coś takiego:
import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution
Jeśli przyjrzymy się, jak Java 8 radzi sobie z tym, zobaczymy, że wszystkie trzy zadania będą uruchamiane w tym samym czasie, asynchronicznie. Pomimo niepowodzenia zadania 2 z wyjątkiem, wynik końcowy zostanie obliczony i wydrukowany pomyślnie.
CompletableFuture
znacznie ułatwia budowanie asynchronicznych zadań z wieloma etapami i daje nam łatwy interfejs do dokładnego definiowania, jakie działania należy podjąć po zakończeniu każdego etapu.
Java Data i godzina API
Jak stwierdzono we własnym przyznaniu Javy:
Przed wydaniem Java SE 8 mechanizm daty i godziny Java był udostępniany przez klasy
java.util.Date
,java.util.Calendar
ijava.util.TimeZone
, a także ich podklasy, takie jakjava.util.GregorianCalendar
. Klasy te miały kilka wad, m.in
- Klasa Calendar nie była bezpieczna pod względem typu.
- Ponieważ klasy były mutowalne, nie mogły być używane w aplikacjach wielowątkowych.
- Błędy w kodzie aplikacji były częste ze względu na nietypową numerację miesięcy i brak bezpieczeństwa typów.”
Java 8 w końcu rozwiązuje te od dawna problemy dzięki nowemu pakietowi java.time
, który zawiera klasy do pracy z datą i godziną. Wszystkie są niezmienne i mają interfejsy API podobne do popularnego frameworka Joda-Time, którego prawie wszyscy programiści Java używają w swoich aplikacjach zamiast natywnych Date
, Calendar
i TimeZone
.
Oto kilka przydatnych klas w tym pakiecie:
-
Clock
— zegar wskazujący aktualny czas, w tym aktualną chwilę, datę i godzinę ze strefą czasową. -
Duration
iPeriod
— ilość czasu.Duration
wykorzystuje wartości oparte na czasie, takie jak „76,8 sekund, orazPeriod
, oparte na dacie, takie jak „4 lata, 6 miesięcy i 12 dni”. -
Instant
— chwilowy punkt w czasie, w kilku formatach. -
LocalDate
,LocalDateTime
,LocalTime
,Year
,YearMonth
— data, godzina, rok, miesiąc lub ich kombinacja bez strefy czasowej w systemie kalendarza ISO-8601. -
OffsetDateTime
,OffsetTime
— data i godzina z przesunięciem względem czasu UTC/Greenwich w systemie kalendarza ISO-8601, na przykład „2015-08-29T14:15:30+01:00”. -
ZonedDateTime
— data i godzina ze skojarzoną strefą czasową w systemie kalendarza ISO-8601, na przykład „1986-08-29T10:15:30+01:00 Europa/Paryż”.
Czasami musimy znaleźć jakąś względną datę, na przykład „pierwszy wtorek miesiąca”. W takich przypadkach java.time
udostępnia specjalną klasę TemporalAdjuster
. Klasa TemporalAdjuster
zawiera standardowy zestaw regulatorów, dostępnych jako metody statyczne. Pozwalają nam one:
- Znajdź pierwszy lub ostatni dzień miesiąca.
- Znajdź pierwszy lub ostatni dzień następnego lub poprzedniego miesiąca.
- Znajdź pierwszy lub ostatni dzień roku.
- Znajdź pierwszy lub ostatni dzień następnego lub poprzedniego roku.
- Znajdź pierwszy lub ostatni dzień tygodnia w ciągu miesiąca, na przykład „pierwsza środa czerwca”.
- Znajdź następny lub poprzedni dzień tygodnia, na przykład „następny czwartek”.
Oto krótki przykład, jak uzyskać pierwszy wtorek miesiąca:
LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
Java 8 w podsumowaniu
Jak widać, Java 8 to epokowe wydanie platformy Java. Wprowadzono wiele zmian językowych, w szczególności wprowadzenie lambd, co stanowi krok w kierunku wprowadzenia bardziej funkcjonalnych możliwości programowania w Javie. Stream API jest dobrym przykładem na to, jak lambdy mogą zmienić sposób, w jaki pracujemy ze standardowymi narzędziami Java, do których jesteśmy już przyzwyczajeni.
Ponadto Java 8 oferuje kilka nowych funkcji do pracy z programowaniem asynchronicznym i bardzo potrzebną modernizację narzędzi do obsługi daty i czasu.
Wszystkie te zmiany razem stanowią duży krok naprzód dla języka Java, czyniąc tworzenie języka Java ciekawszym i wydajniejszym.