Java 8 – co nowego i co ciekawego?

Autor: Patryk Stopyra, Software Developer, Empirica S.A.

Java Development Kit 8 jest jedną z najważniejszych aktualizacji języka rozwijanego przez Oracle. Niesie ona za sobą przede wszystkim istotne zmiany na poziomie syntaktycznym wprowadzając wyrażenia znacznie upraszczające zapis logiki programu. Jest to ważne zwłaszcza dla deweloperów oprogramowania rozwijających projekty zespołowe. Ze względu na liczbę istotnych zmian – usystematyzujemy je, dzieląc na 4 dziedziny.

Język

Najbardziej zauważalną dla programisty zmianą w języku Java w wersji 8 są wyrażenia lambda (λ). Są to funkcje anonimowe, swą nazwę czerpiące z rachunku lambda opracowanego przez A. Churcha i S.C. Kleenego w latach 30-tych XX-go wieku w celu reprezentacji i badania algorytmów. Jak w języku Java zrealizowano wyrażenia lambda? Wykorzystano już istniejące struktury semantyczne – odtąd anonimowa implementacja interfejsu posiadającego jedną metodę może być zastąpiona przez wyrażenie lambda – oraz dodano operator ->. Samo wyrażenie ma następującą postać:

(arg1, arg2, …) -> { instr1; instr2; ... }

Wykorzystując więc zestaw przekazywanych argumentów maszyna wirtualna wykonuje blok instrukcji, który w istocie jest ciałem anonimowej metody. Zapis ten można skrócić jeszcze bardziej: jeśli przyjmujemy tylko jeden argument możemy pominąć nawiasy po lewej stronie operatora ->, zaś jeśli program wykonuje tylko jedną instrukcję możemy nie zamieszczać nawiasów po prawej stronie operatora ->. W efekcie dla przykładowego interfejsu:

interface SomeListener {
    void actionPerformed(SomeEvent e);
}

programista może zastąpić zapis jego implementacji:

mysteriousComponent.addSomeListener(new SomeListener {
    @Override
    void actionPerformed(SomeEvent e) {
        System.out.println(“Action occured: “ + e);
    }
});

bardziej zwięzłą i czytelną formą:

mysteriousComponent.addSomeListener(e -> System.out.println(“Action occured: “ + e));

Zastosowane w ten sposób wyrażenie lambda definiuje jedyną metodę interfejsu stając się w istocie anonimową implementacją interfejsu – i to właśnie sposób w jaki programista powinien je postrzegać.

Co więcej, dzięki nowemu słowu kluczowemu default (o którym za chwilę) deweloper może używać wyrażeń lambda nawet dla bardziej złożonych interfejsów.

Naturalnym jest, iż wyrażenia lambda zwracają wartość. I w tym przypadku można zastosować kompaktową notację. Następujący kod:

(x, y) -> {return x + y;}

jest równoważny z:

(x, y) -> x + y

Należy mieć więc na uwadze, że wyrażenie zwraca wartość pojedyńczej instrukcji jaką wywołuje (chyba, że wywołuje metodę o typie void).

Wyrażenia lambda znajdują doskonałe zastosowanie m.in. w operacjach na strumieniach. Te (opisane dalej) również są nowością najświeższej wersji języka

Java, w której trafiły do biblioteki standardowej.

Nowe słowo kluczowe default pozwala zapwnić domyślną implementację metod interfejsów, co ułatwia tworzenie wyrażeń lambda opartych na tychże interfejsach. Metod z domyślną implementacją nie trzeba już nadpisywać. Tworząc więc interfejs którego wszystkie metody są domyśle prócz jednej, zależącej od kontekstu wykorzystania interfejsu, uzyskujemy perfekcyjny fundament pod wyrażenie lambda. W docelowym miejscu wystarczy (w postaci takiego wyrażenia) zapewnić implementację jedynej metody nieposiadającej implementacji domyślnej. Jest to niewątpliwy ruch języka Java w stronę wielokrotnego dziedziczenia.

Kolejną istotną dla dewelopera zmianą jest możliwość przekazywania referencji do metod. Dotąd w języku Java możliwe było jedynie przekazywanie wartości zmiennych i referencji do nich. Nowością syntaktyczną w JDK 8 jest natomiast operator :: pozwalający na przekazywanie referencji do zaimplementowanych już metod. Na przykład:

Math::max, Math::abs

Ta zmiana jest również de facto konsekwencją wprowadzenia wyrażeń lambda. Czyni ona kod czytelniejszym, gdyż pozwala (zamiast wielokrotnej implementacji identycznych wyrażeń) w jednym miejscu zdefiniować odpowiednią metodę i odwoływać się bezpośrednio do niej z wielu miejsc. Mając klasę, w której zaimplementowaliśmy pewien sposób reakcji na zdarzenia:

class Controller {
    static void reactOnEvent() {
        …
    }
}

Możemy zastąpić kod:

mysteriousComponent.addSomeListener(e -> Controller.reactOnEvent());

przez:

mysteriousComponent.addSomeListener(Controller::reactOnEvent);

Dodatkowo bytecode tak zdefiniowanego listenera różni się od wykorzystującego wyrażenia lambda. Nie zawiera on całej definicji Listenera lecz krótszą bezpośrednią referencję do metody reactOnEvent. Referencje do metod (nawet niestatycznych) programista może przechowywać jako zmienne wykorzystując klasy z biblioteki standardowej java.util.function.

Oprócz nowości syntaktycznych, Java 8 wprowadza również kilka udoskonaleń istniejących już wcześniej mechanizmów. Na pozór niezauważalną dla dewelopera zmianą jest ulepszenie inferencji typów. W obecnej wersji język jest w stanie w niektórych przypadkach typów generycznych sprowadzić automatycznie klasę obiektu do klasy niższej, jeśli tylko nie ma to skutków ubocznych. Przytaczając przykład z dokumentacji Oracle, w metodach przyjmujących listę obiektów typu String nie musimy już stosować konstrukcji:

Collections.emptyList()

w celu pozbycia się niejednoznaczności i zaakceptownaia pustej listy jako adekwatnej dla metody z argumentem listy obiektów typu String. W JDK 8 możemy wywołać:

Collections.emptyList()

zaś sam kompilator, odkrywszy, że nie ma to skutków ubocznych – przystosuje listę do typu wymaganego przez metodę.

READ  Jak znaleźć pracę w Web3? Najpopularniejsze oferty pracy związane z kryptowalutami i Blockchain w 2023 r.

Zapewne chcąc sprostać rosnącej popularności weryfikacji formalnej oraz programowania kontraktowego, środowisko odpowiedzialne za rozwój języka Java wzbogaciło go o nowe adnotacje oraz rozszerzyło zakres miejsc w kodzie w których te mogą zostać wykorzystane. To właśnie adnotacje takie jak @NonNull, @Readonly, @Regex, itp. pełnią w języku Java rolę kontraktów. Programista może je teraz stosować w praktycznie dowolnym miejscu:

List<@NonNull MysteriousObject>, matches(@Regex String s)

Statyczne analizatory programu potrafią wykorzystać takie adnotacje jako dyrektywy automatycznej weryfikacji kodu, na ich podstawie badając poprawność założeń programisty. Nowe adnotacje, rozsądnie wykorzystane i połączone z automatyczną ich analizą pozwalają zwiększyć czytelność kodu. Niefortunnie jednak ich nadmiar potrafi sam kod zaciemniać, ułatwiając pracę z nim komputerowi ale utrudniając innym deweloperom zespołu autora. Dlatego warto tę funkcjonalność stosować z umiarem, mając przede wszystkim na uwadze czytelność tworzonego algorytmu.

Jak wspomniano na wstępie dobre opanowanie wyrażeń lambda, referencyjności metod i innych zmian pojawiających się w syntaktyce ósmej wersji języka Java jest podstawą do tworzenia bardziej przejrzystego i funkcjonalnego kodu. Natomiast w pracy zespołowej jest kluczem do szybszego rozumienia algorytmów innych programistów i tworzenia jednoznacznie interpretowalnego kodu.

Biblioteka standardowa

Jak przy każdej aktualizacji wiele ciekawych zmian pojawiło się również w bibliotece standardowej języka Java. Warto się im przyjżeć, ale zostanie to zrobione bardziej pobieżnie gdyż aktualizacje te, choć istotne, nie są aż tak przełomowe jak wyżej opisane unowocześnienia samego języka.

Pierwszym dużym postępem jest wprowadzenie do języka standardowej implementacji strumieni. Te (tylko bez kontroli typów) znane są dobrze programistom Pythona. Z punktu widzenia dewelopera strumień jest bardzo atrakcyjną konstrukcją. Nie magazynuje danych, lecz przepuszcza je kolejno korzystając z kolekcji, tablicy bitów, pliku czy innego źródła. Jest łatwy do implementacji, rozszerzania i dokonywania innych modyfikacji: praca na strumieniu opiera się na konstrukcji nazywanej pipeline, ze względu na swoje podobieństwo do rurociągu. Po pozyskaniu strumienia możemy wykonywać na nim kolejne operacje nieterminalne, wykorzystując schemat budowniczego (możemy stosować filtry, mapowanie elementów, itp.; każda metoda dodająca nieterminalną operację do strumienia zwraca nowy, zmodyfikowany strumień). Na końcu dodajemy operację terminalną, np. forEach():

collectionOfComponents.stream()
    .filter(c -> c instanceof JButton)
    .filter(c -> c.isVisible(true))
    .forEach(c -> c.setActive(false));

W efekcie otrzymujemy pipeline zbudowany z łatwo wymienialnych segmentów. Ponadto natura strumieni jest leniwa, tzn. nie podejmują kolejnego działania bez zakończenia przetwarzania poprzedniego elementu w strumieniu. Dzięki temu w wypadku metod w rodzaju znajdź pierwszy, znajdź jakikolwiek nie musimy przetwarzać wszystkich danych, lecz możemy przerwać gdy ostatni segment z powodzeniem zakończy pracę. Co ciekawe strumienie są szybsze od standardowej iteracji nawet gdy wykonują operacje w których muszą zaaplikować coś do wszystkich elementów kolekcji, co jest kolejnym silnym argumentem przemawiającymza ich stosowaniem. Więcej informacji o strumieniach można znaleźć w dokumentacji java.util.stream.

Najbardziej naturalnym sposobem definiowania segmentów pipeline’u są wyrażenia lambda. Te wykorzystane w konstrukcji strumieni implementują interfejsy funkcji takie jak Consumer, Function<t,r></t,r>, etc.. Można je znaleźć w paczce java.util.function. Dzięki wspomnianej wyżej zaawansowanej inferencji typów można je z powodzeniem wykorzystywać bez potrzeby wskazywania zwracanych i przyjmowanych typów explicite.

Java 8 wnosi wiele również w dziedzinie programowania równoległego. Praca ze strumieniami pozwala na natychmiastowe i bezbolesne przejście w tryb współbieżny za pomocą zmiany jednej tylko metody (stream() na parallelStream()). Mechanizm można wykorzystywać również na obiektach, nie będących thread-safe (tak długo jak długo nie próbujemy zmieniać ich zawartości, a jedynie pracować na pobranych wartościach). Oprócz równoległych strumieni czas wykonania naszego równoległego algorytmu może znacznie się zmniejszyć także dzięki wykorzystaniu nowych metod Arrays.parallelSort() (doskonale działającego dla dużych tablic) czy nowym akumulatorom (LongAccumulator, DoubleAccumulator z paczki java.util.atomic), które pozwalają na wykonywanie operacji na zmiennych z wielu równoległych wątków jeszcze wydajniej, niż w przypadku wcześniej wprowadzonych obiektów Atomic. Także ConcurrentHashMap (java.util.concurrent.ConcurrentHashMap) doczekał się optymalizacji wraz ze standardową HashMapą – przechowywanie wartości znajdujących się pod jednym hashem w postaci zbalansowanego drzewa pozwala na zmniejszenie pesymistycznego czasu dostępu do elementu z O(n) do O(log n).

Z punktu widzenia programisty pracującego nad aplikacjami biznesowymi atrakcyjną zmianą będzie nowe API daty i czasu. Wykorzystanie go wiąże się niestety z koniecznością licznych zmian w miejscach w których używano dotychczasowych klas Date oraz Time. Niesie ono jednak za sobą wymierne korzyści oraz przewagę nad dotychczasowym, nieintuicyjnym zarządzaniem czasem. Podstawowe założenia nowego api to niemutowalność obiektów reprezentujących datę/czas (co znacząco wpływa na bezpieczeństwo kodu), wprowadzenie klasy kompozytowej łączącej funkcjonalność czasu i daty, czytelność metod, łatwość obsługi stref czasowych, konwersji oraz operacji pozyskania nowych obiektów przez przekształcenie istniejących. Przy czym implementacja ostatniego założenia nasunie wielu deweloperom na myśl dobrze sprawdzający się mechanizm mutacji obiektów klasy Calendar. Nowe klasy: LocalTime, LocalDate, LocalDateTime znajdują się w paczce java.time i warte są uwagi – ich wdrożenie niewątpliwie zwiększa niezawodność i łatwość utrzymania aplikacji biznesowych, zwłaszcza tych o potencjale globalnym.

READ  12 cech, które dobry programista posiadać powinien

Nowa Java to standardowo poprawa bezpieczeństwa. Tym razem ulepszono standardowe algorytmy zabezpieczeń opartych na haśle, generatory pseudolosowe o zwiększonej entropii, wsparcie dla bardziej zaawansowanego dostępu do zasobów http z wykorzystaniem security manager’a, ustawienie protokołu TLS 1.2 jako domyślnego po stronie klienta, zwiększono wsparcie dla systemowych schematów kryptograficznych na różnych systemach operacyjnych, dodano również klasę reprezentującą kod Base64 oraz jego dekodery w bibliotece standardowej (java.util.Base64) ułatwiającą pracę z tym popularnym kodowaniem. Są to jednak zmiany i ułatwienia o bardzo specyficznych zastosowaniach.

Ciekawą, choć dla wielu programistów niezauważalną zmianą pozostanie możliwość interpretacji obiektów reprezentujących liczby całkowite jako liczb typu unsigned. Uczyniono to przez rozszerzenie klas Integer i Long o nowe metody porównań i działań. Ulepszenie to nie wpływa na standardową interpretację tych obiektów, pozwala jednak wykonywać na nich nowe operacje. Jest to ukłon w stronę deweloperów kompaktowych zastosowań języka Java, np. w urządzeniach pomiarowych czy transmiterach oraz tych, którzy korzystają z bibliotek i protokołów utworzonych w oparciu o język C. Niewątpliwie w wielu miejscach ulepszenie to ułatwia nterpretację, jednak należy podchodzić do niego z pewną dozą ostrożności – do zastosowań krytycznych wciąż brakuje w języku Java w tej kwestii sposobów na wymuszenie jednoznacznej interpretacji obiektu Integer jako unsigned, co potencjalnie mogłoby doprowadzić do trudnych do zdiagnozowania błędów w systemie.

W najnowszej wersji języka Java nie mogło zabraknąć ulepszeń bardzo silnie rozwijanej technologii interfejsu użytkownika: JavaFX.

Modena theme look & feel (fxexperience.com)

Oracle promuje przede wszystkim nowy motyw Modena, możliwość zakorzenienia komponentów Swing w aplikacji JavaFX dzięki klasie SwingNode, rozwinięty sposób zarządznia wyświetlanym tekstem, rozszerzenie opcji manipulowania grafiką trójwymiarową oraz rosnące wsparcie dla HTML5 i CSS.

Maszyna Wirtualna

Każda aktualizacja pakietu Java Development Kit niesie za sobą szereg mniejszych zmian i poprawek maszyny wirtualnej dostarczanej przez Oracle. Nowa wersja języka to okazja do większych poprawek. W najświeższej wersji maszyny wirtualnej dostarczonej wraz z 8. edycją języka pojawiły się dwa znaczące ulepszenia.

Jednym z nich są kompaktowe profile – pozwalające tworzyć mniejsze dystrybucje programów, korzystające tylko z selektywnie dobranych podzbiorów funkcjonalności maszyny wirtualnej. Takie odchudzone dystrybucje można później deployować na małych urządzeniach w których pamięć jest najbardziej krytycznym zasobem.

Drugie udoskonalenie to nowy interpreter JavaScript o nazwie Nashorn. Można wywołać go bezpośrednio z kodu programu korzystając z klasy ScriptEngine lub ewaluować za jego pomocą kod korzystając z takich narzędzi jak jjs czy jrunscript.

Narzędzia

Omawiając nowości w języku Java 8 grzechem byłoby nie wspomnieć o zmianach w narzędziach dostarczanych programiście. I jest ich kilka. Pojawiło się, wspomniane już powyżej, narzędzie jjs wywołujące interpreter Nashorn. Nowy jest także jdeps przygotowany z myślą o analizie plików class. Za pomocą polecenia java możemy teraz uruchamiać aplikacje pisane w technologii JavaFX. Z kolei narzędzie jarsigner zyskało nową opcję zarządania znakowania czasowego pliku (trusted timestamping) wykonywanego przez wskazane Time Stamping Authority. Ulepszenia doczekało się dobrze znane narzędzie javadoc służące generowaniu dokumentacji źródeł, dodano m.in. zdolność tworzenia abstrakcyjnych drzew syntaktycznych na podstawie utworzonego w programie kodu.

Również najbardziej istotne dla dewelopera narzędzie javac, zyskało swoje udoskonalenia. Może być teraz wywołane z dyrektywą -parameters wykorzystywaną do przechowywania formalnych nazw parametrów, potrafi również weryfikować zawartość javadoc’ów oraz generować pliki nagłówkowe, w której to funkcjonalności zastępuje javah.

***

Już pobieżne spojerznie na powyższe zmiany doskonale ilustruje szeroką płaszczyznę optymalizacji i ulepszania biblioteki standardowej języka Java. Świadczy to niewąpliwie o żywotności i rozwoju tego języka. Przed deweloperami wyraźnie zarysowują się główne nurty wspomnianego rozwoju – wyrażenia lambda, zwiększanie czytelności kodu poprzez podwyższanie poziomu abstrakcji i zmniejszanie jego objętości, wydajne zrównoleglanie programów, jeszcze ciaśniejsze wiązanie się języka Java z zastosowaniami wysokiego ryzyka (w tym biznesowymi). Cieszy zwłaszcza ostatnia dziedzina rozwoju – Java została przecież stworzona do zastosowań biznesowych, a rosnący poziom bezpieczeństwa języka jest jednym z gwarantów niezawodności tworzonych w nim aplikacji.

Bibliografia

D.Harel., Y.Feldman, Rzecz o istocie informatyki, Wydawnictwo Naukowo-Techniczne, Warszawa 2008

http://fxexperience.com/2013/03/modena-theme-update/

http://www.javacodegeeks.com/2013/04/arrays-sort-versus-arrays-parallelsort.html

http://www.journaldev.com/2800/java-8-date-time-api-example-tutorial-localdate-instant-localdatetime-parse-and-format

http://www.mimuw.edu.pl/~urzy/Lambda/erlambda.pdf

http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html

https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html http://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/intro.html

https://blogs.oracle.com/java-platform-group/entry/java_8_s_new_type

http://www.oracle.com/technetwork/articles/java/jf14-date-time-2125367.html

http://www.oracle.com/technetwork/java/javase/8-whats-new-2157071.html