Java 9 – co nowego?

Opublikowane przez Piotr Wachulec w dniu

Cześć!

Chyba jeszcze nie było takiego wpisu u mnie na blogu. Chciałbym w tym wpisie powiedzieć Ci w kilku słowach o nowościach w języku Java, w jego najnowszej wersji 1.9.

Trochę przegapiłem premierę tej wersji języka (a jest to jeden z moich ulubionych!). Wybrałem się na konferencję JavaCon (wydarzenie na Facebooku oraz opis wydarzenia na Evenea) i tam miałem okazję usłyszeć o największych nowinkach dotyczących tej technologii.

Oczywiście spis wszystkich zmian znajdziesz także w dokumentacji Oracle.

No to co takiego ciekawego znalazło się w Javie 9?

Moduły

Programiści cenią modularność. Chcą budować duże aplikacje z mniejszych klocków. Podstawową jednostką w Javie są klasy, które łączą się w pakiety spakowane w pliki jar. Jednak przy większej ilości klas, pakietów i plików można się łatwo zgubić, co jest do czego. Dlatego zdecydowano się na wprowadzenie dodatkowej warstwy abstrakcji, umożliwiającej grupowanie pakietów bezpośrednio ze sobą współpracujących i zintegrowanych w większe, logiczne jednostki.

A module is a set of packages designed for reuse.

Programiści Oracle dodali do języka funkcjonalność łączenia pakietów w grupy zwane modułami.  Jest to kluczowa zmiana w tej wersji języka oznaczona w dokumentacji jako Key Changes. Program zbudowany modułów jest bardziej niezawodny niż zbudowany z pakietów i plików jar, z nadmiarem dostępnych API… Moduły zapewniają silną enkapsulację. Możemy wyeksportować klasy, które będą publiczne dla wszystkich, dla “zaprzyjaźnionych modułów” oraz tylko wewnątrz modułu.

Definicja widoczności pakietów, co wchodzi w skład całego modułu oraz zależności od innych modułów umieszczana jest w pliku module-info.java. Dzięki modułom jesteśmy w stanie uniknąć brakujących zależności, cyklicznych zależności czy rozbicia pakietów. No i należy pamiętać, że moduły nie są obowiązkowe. Java 9 wspiera także pisanie aplikacji w tradycyjnym stylu.

W związku z powyższą zmianą, całe JDK zostało przebudowane. Schemat zależności modułów możecie zobaczyć na diagramie poniżej.

Migracja

Wprowadzenie tej modyfikacji ma wiele zalet, ale budzi także strach w oczach twórców różnych bibliotek. Z jednej strony umożliwia łączenie pakietów w logiczne całości i podpinanie ich w zależności od potrzeb. Dzięki temu możemy odchudzić JDK, co umożliwia uruchamianie maszyny wirtualnej na urządzeniach o zdecydowanie mniejszych zasobach oraz umożliwia wzrost wydajności, ułatwia napisanie bezpiecznej aplikacji, a także ułatwia jej utrzymanie.

A przed czym drżą twórcy bibliotek? A no przed tym, że korzystają oni z fragmentów biblioteki standardowej, które są przeznaczone do użytku wewnętrznego przez komponenty JDK. JDK9 czasowo pozwala na dostęp do wewnętrznych fragmentów JDK i informuje o tym odpowiednim ostrzeżeniem w czasie kompilacji.

Opis w kodzie

W ogólności opis modułu wygląda następująco:

module ${module-name} {
        requires ${module-name};
        exports ${package-name};
}

Fabryki dla kolekcji

Przeanalizujmy sposób, w jaki np. tworzyliśmy listę obiektów predefiniowanych.

List<Foo> myFooList = new ArrayList<>();
myFooList.add(new Foo(32));
myFooList.add(new Foo(33));
myFooList.add(new Foo(21));
myFooList.add(new Foo(54));
myFooList = Collections.unmodifiableList(myFooList);

Dzięki Javie 9 nie będzie już konieczne takie rzeźbienie.

List<Foo> myFooList = List.of(new Foo(32),
                              new Foo(33),
                              new Foo(34),
                              new Foo(21),
                              new Foo(54));

Bardziej zabieg kosmetyczny, jednak wydaje się dość przyjemną zmianą (szczególnie biorąc pod uwagę, że programiści to takie leniwe zwierzaki).

Ponadto:

  • są zoptymalizowane pod względem zajmowanego miejsca (dzięki zastosowaniu nowych interfejsów)
  • przechodzenie po zbiorze i mapie jest losowe
  • jeżeli elementy w kolekcji są serializowalne, to kolekcję da się zserializować,
  • nie można dodawać null do kolekcji,
  • kolekcje są niezmienne, mówimy też o efektywnej niezmienności, jeżeli kolekcja zawiera elementy niezmienne (np. Stringi).

JShell

Podstawowe pojęcie – REPL: (ang. read-eval-print loop) – jest to proste, interaktywne środowisko programowania, które pozwala na szybkie uruchamianie kodu programu. Wprowadzamy kod w konsoli i dostajemy wynik wykonania bezpośrednio na ekran.

Java doczekała się swojego REPLa dostarczanego wraz z JDK. Zwie się to JShell. Oczywiście już wcześniej istniały takie rozwiązania, np. Java REPL czy Repl.it. Fajnie, że twórcy języka postanowili dostarczyć (w końcu) to narzędzie wraz z pakietem programistycznym. Takie środowisko jest przydatnym narzędziem w codziennej pracy, kiedy potrzebujemy coś sprawdzić na szybko, czy zwyczajnie się pobawić jakimiś nowymi rozwiązaniami. Wtedy nie potrzebne jest zakładanie nowego projektu, kompilacja itd. tylko piszemy to, czego w danej chwili potrzebujemy. Można też w szybki sposób tworzyć prototypy rozwiązań.

JShell jest głęboko zintegrowany z JDK, dzięki czemu jest cały czas aktualny i kompatybilny z najświeższą wersją języka. Dodatkowo posiada API umożliwiające integrację z innymi aplikacjami (np. jeżeli chcielibyśmy osadzić takie środowisko w swojej aplikacji). Instaluje się wraz z instalacją JDK 9 (jeżeli nie działa od razu komenda jshell w konsoli, to program znajduje się w miejscu, gdzie zainstalowaliśmy JDK w katalogu bin).

JShell daje możliwość deklaracji kodu i jego wykonywania, ale także udostępnia kilka komend. Ponadto działa dopełnianie kodu tabulatorem, edytowanie linii oraz historia wpisywania.

Jeżeli chcemy wykorzystać JShell w programie, to jest dostępny w pakiecie jdk.jshell.JShell. Na blogu Jakuba Dziworskiego znajdziecie przykłady użycia JShella.

Drobne zmiany językowe

Project Coin JDK 7

Projekt Coin zawiera zmiany, które były proponowane do dodania w JDK 7. Część z nich jednak nie zostało wprowadzonych i ponownie były rozważane przy wprowadzaniu JDK 8. Koniec końców kilka z nich zostało ulepszonych w JDK 9.

@SafeVarargs

Przy tworzeniu generycznej metody ze zmienną listą argumentów

void foo(T... args);

mogliśmy napotkać takie ostrzeżenie

warning: [unchecked] unchecked generic array creation for varargs parametr of type...

Związane jest to z tzw. heap pollution. Z heap pollution mamy do czynienia, kiedy zmienna sparametryzowana odnosi się do obiektu, który nie jest typu taki, jak ten z parametru. W czasie działania programu może to się skończyć wyjątkiem ClassCastException.

Typ adnotacji (java.lang.SafeVarargs), który zapobiega występowaniu powyższego ostrzeżenia, często nie mówiącego tak naprawdę nic (ze względu na to, że wyjątek nie zostanie rzucony). W przykładzie powyżej, heap pollution jest osiągany ze względu na transformacje zmiennej listy parametrów podczas kompilacji programu. W czasie kompilacji lista argumentów jest przekształcana do tablicy i kiedy typ jest znany, to nie ma problemu. A kiedy nasza metoda jest generyczna, to Java w czasie kompilacji nie wie, tablice jakiego typu utworzyć, więc domyślnie tworzy tablicę typu Object. Jednak w runtime typ obiektów w tablicy musi być znany, dlatego powoduje to takie ostrzeżenie i w większości przypadków nie jest to nic groźnego. Adnotacja @SafeVarargs powoduje, że owe ostrzeżenie się nie wyświetla. Można jej użyć tylko i wyłącznie w klasach, na metodach, które nie mogą zostać nadpisane: final, statickonstruktory oraz właśnie w Java 9 dozwolono użycia adnotacji przy metodach prywatnych. Funkcja sama w sobie dodana została w Javie 7.

try-with-resources

Konstrukcja try-with-resources jest tak zwanym cukrem syntaktycznym, tzn. że dzięki nim kod staje się prostszy, bardziej czytelny, jednak można sobie bez nich świetnie poradzić. Przykład? Proszę bardzo.

BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(inputFile));
            br.readLine();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } 
        }

Widzimy ile się trzeba napisać. A konstrukcja try-with-resources sama nam wywoła metody close() tam, gdzie jest to potrzebne. A kod wygląda dzięki temu tak:

try (BufferedReader br = new BufferedReader(new FileReader(inputFile))) {
            br.readLine();
        } catch (Exception e) {
            e.printStackTrace();
        }

W Java 7 zmienne musiały być zadeklarowane w konstrukcji

try (Resource r = ...)

Zaproponowano poniższe rozwiązanie

Resource r = new Resource1();
        try (r) {
            r = new Resource2();
        }

W powyższym przypadku, dla którego źródła powinna być wywołana metoda close()? No właśnie. Zatem dodano informację, że źródłem może być tylko zmienna, która jest final lub effectively final, czyli nie możemy modyfikować źródła wewnątrz konstrukcji.

Diamond c. d.

W JDK 7 umożliwiono programistom stosowanie krótszego zapisu przy konstrukcji obiektów klas generycznych.

List<Map<Foo, Bar>> myFooList = new ArrayList<Map<Foo, Bar>>();

Od JDK 7 można to zastąpić dzięki wnioskowaniu typów “diamentem”

List<Map<Foo, Bar>> myFooList = new ArrayList<>();

A co, jeśli chcielibyśmy nadpisać metody z użyciem klasy anonimowej?

List<Map<Foo, Bar>> myFooList = new ArrayList<>() {
    // @Override
}

I nie działa 🙁 A dlaczego?

Internally, a Java compiler operates over a richer set of types than those that can be written down explicitly in a Java program. The compiler-internal types which cannot be written in a Java program are called non-denotable types. Non-denotable types can occur as the result of the inference used by diamond. Therefore, using diamond with anonymous inner classes is not supported since doing so in general would require extensions to the class file signature attribute to represent non-denotable types, a de facto JVM change. It is feasible that future platform versions could allow use of diamond when creating an anonymous inner class as long as the inferred type was denotable.

Co w wolnym tłumaczeniu może brzmieć:

Wewnętrznie kompilator Java operuje nad bogatszym zbiorem typów niż te, które mogą być wyraźnie zapisane w programie. Wewnętrzne typy zakodowane w kompilatorze, które nie mogą być zapisane w programie są nazywane typami nieoznaczonymi. Typy nieoznaczone mogą występować jako rezultat wnioskowania wykorzystywanego przez konstrukcję diamentową. Dlatego używanie konstrukcji diamentowej z wewnętrzną klasą anonimową nie jest wspierane, ponieważ generalnie wymagałoby to rozszerzenia podpisu pliku klasy do reprezentacji typów nieoznaczonych, czyli de facto zmiany wirtualnej maszyny Javy. Jest prawdopodobne, że przyszłe wersje platformy mogłyby pozwolić używać konstrukcji diamentowej przy tworzeniu wewnętrznej klasy anonimowej tak długo, aż wnioskowany typ będzie oznaczony.

W JDK 9 maszyna wirtualna została zmodyfikowana tak, że zdanie pochylone w cytacie, jest już prawdziwe 🙂

Project Lambda JDK 8

Podobna historia jak z Projektem Coin.

Podkreślnik _ 

Konwencja była prosta – jeżeli nie obchodzi Cię dana zmienna, to możesz nadać jej identyfikator _. Zazwyczaj używano tego w wyrażeniach lambda.

Od Java 8 _ był zakazany w wyrażeniach lambda.

_ -> { // some code here }

Od Java 9 identyfikator jest całkowicie zakazany, ponieważ planowane jest wykorzystanie go jako element składni języka.

Metody prywatne w interfejsach

Początkowo interfejsy pozwalały tylko na deklarowanie metod publicznych. W Javie 8 dodano metody domyślne, będące nieabstrakcyjnymi i dlatego mogły posiadać ciało. Na poziomie maszyny wirtualnej Javy interfejsy mogłyby posiadać metody prywatne, pomocne w implementacji wyrażeń lambda itd., ale sam język na to nie pozwalał.

W Javie 9 dodano możliwość definiowania metod prywatnych oraz prywatnych metod statycznych. Podsumowując wszystkie możliwości interfejsów:

  • stałe,
  • metody abstrakcyjne,
  • metody domyślne,
  • metody statyczne,
  • metody prywatne,
  • prywatne metody statyczne.

Rozszerzenie adnotacji @Deprecated

W Javie 5 dodano adnotację @Deprecated, której zadaniem było wskazywanie fragmentów skazanych na zagładę. Podczas kompilacji dostajemy ostrzeżenie o korzystaniu z kodu oznaczonego jako przestarzałego. Teoretycznie można było to wyłączyć inną adnotacją, jednak w praktyce kompilatory to ostrzeżenie i tak przepychały do naszej konsoli.

Jednak sama informacja, że coś jest przestarzałe, nam nie wystarczała. Nie było informacji o tym, czy dane API jest planowane do usunięcia, czy też nie. Dlatego zdecydowano się na dodanie do adnotacji dwóch informacji – czy oznaczony kod będzie w przyszłości usunięty oraz od której wersji został oznaczony jako przestarzały: @Deprecated(since=”9″, forRemoval=false). Dostaliśmy także nowe narzędzie analizy kodu jdeprscan, które przeanalizuje nasze pliki klas i pliki jar, a potem poinformuje o tym, czy korzystają z przestarzałego API w JDK.

G1 Garbage Collector

Od wersji JDK 9, domyślnym garbage collectorem będzie G1. Cechuje się zachowaniem dobrego balansu pomiędzy wydajnością i niskimi opóźnieniami. Dzieli stos na wiele mniejszych regionów oraz wyszukuje wszystkie żywe obiekty współbieżnie, dzięki czemu aplikacja nie jest zatrzymywana.

JDK9 a kontenery

Od niedawna świetną alternatywą dla maszyn wirtualnych są kontenery, które pozwalają na uruchomienie aplikacji w wydzielonym właśnie takim kontenerze, jednak bez emulowania warstwy sprzętowej i systemu operacyjnego. JDK 9 uwzględnia ograniczenia pamięci i procesora narzucone przez kontenery, a sama maszyna wirtualna Javy została zaprojektowana tak, aby zachować stabilność przy zmiennych zasobach. Dzięki modułowości możliwe jest odchudzenie samego JDK. Programiści Oracle dostarczają narzędzie takie jak jlink, które pozwala na wykreowanie swojego środowiska uruchomieniowego Javy. Środowisko może być ściśle dopasowane do możliwości sprzętowych oraz do rzeczywistego zapotrzebowania na moduły dostarczane przez Oracle.

Ulepszenia Stream API

Strumienie są narzędziem dostarczonym także z Javą 8. Strumień jest sekwencją elementów, jednak sam ich nie przechowuje – znajdują się one w jakimś źródle, np. kolekcji. Konstruuje połączenie różnych operacji na tych elementach.

Java 9 dodaje dodatkowe metody:

  • takeWhile(Predicate)dropWhile(Predicate) – działanie podobne do metod limit()skip(), jednak przyjmuja predykaty (predykaty są to jednoargumentowe funkcje logiczne), takeWhile() bierze elementy ze strumienia wejściowego i przekazuje je do strumienia wyjściowego, dopóki predykat jest spełniony, zaś dropWhile() odwrotnie, no i należy pamiętać, że jeżeli strumień będzie nieposortowany, to możemy otrzymać nieprawidłowe wyniki,
  • ofNullable(T t) – zwraca strumień zera lub jednego elementów w zależności od tego, czy przekazana wartość jest null. Związana jest z nową metodą stream() w klasie Optional,
  • iterate() – metoda ta została przeciążona, w podstawowej wersji działa w nieskończoność i ograniczamy jej działanie poprzez wywołanie limit(), w najnowszej wersji przyjmuje trzeci parametr, właśnie to ograniczenie działania, co przypomina składniowo argumenty pętli for.

Rozszerzenie klasy Optional

Java 8 dostarczyła klasę Optional, która pozwalała na redukcję występowania NullPointerException. Najlepiej na przykładzie:

public class TestProgram {

    public static void main(String[] args) {
        Foo foo = new Foo();
        
        if (foo.getBar().equals("Bar")) {
            System.out.println("It works!");
        }
    }
}

public class Foo {
    
    public Foo() {}
    
    private String bar;
    
    public void setBar(String newBar) {
        bar = newBar;
    }
    
    public String getBar() {
        return bar;
    }
}

Widzimy, że poleci wyjątek, ponieważ String nie został w żaden sposób zainicjalizowany. Powinniśmy sprawdzić, czy nie jest przypadkiem nullem zanim dokonamy porównania. No i na pomoc przychodzi klasa Optional.

public class TestProgram {

    public static void main(String[] args) {
        Foo foo = new Foo();
        
        System.out.println(foo.getBar().orElse("Empty string!"));
    }
}

public class Foo {
    
    public Foo() {}
    
    private String bar;
    
    public void setBar(String newBar) {
        bar = newBar;
    }
    
    public Optional getBar() {
        return Optional.ofNullable(bar);
    }
}

W JDK 9 do klasy Optional zostały dodane 4 nowe metody (w ogólności klasa Optional została wzbogacona o Stream API):

  • ifPresent(Consumer action) – możemy wykonać działanie bezpośrednio na wartości opakowanej przez Optional,
  • ifPresentOrElse(Consumer action, Runnable emptyAction) – j. w. + możemy wykonać jakąś akcję w przypadku braku wartości,
  • or(Supplier supplier) – do tej pory mieliśmy tylko dostępną metodę orElse() lub orElseGet(). Teraz mamy możliwość zapewnienia, że zawsze otrzymamy Optional – jeżeli wartość jest dostępna, to tego samego, jeśli nie, to wygenerowanego z Supplier,
  • stream() – pozwala traktować Optional jako Stream.

Rozszerzenie Process API

W Javie 9 została rozszerzona możliwość interakcji bezpośrednio z systemem operacyjnym. Dostaliśmy narzędzia do bezpośredniego wyciągania PID, nazw procesów, listowanie aktualnych procesów itd. w zdecydowanie prostszy sposób niż do tej pory. Często także konieczne było implementowanie na różne sposoby funkcjonalności w zależności od tego, na jakiej platformie będzie uruchamiany program.

Wsparcie HTTP/2

Specyfikacja standardu HTTP w wersji drugiej dostała oficjalne błogosławieństwo w maju 2015 (RFC 7540). Java 9 posiada pełne wsparcie dla tej wersji protokołu. Dodatkowo oddano do badania przez użytkowników moduł zawierający całkowicie nowe biblioteki służące do komunikacji oraz w przyszłości być może zastąpią całkowicie HttpURL Connection (okaże się to przy wydawaniu wersji Java 10).

Prezentacja

O powyższym miałem okazję opowiedzieć na wydarzeniu Fire Talk: Trends in Java organizowanym przez firmę Sharpeo. Byłem patronem medialnym tego wydarzenia 🙂 Możesz pobrać streszczenie tego tekstu w postaci slajdów z mojej prezentacji.

Podsumowanie

Wszystkich zmian w JDK było 150. Nie sposób o nich wszystkich opowiedzieć/napisać. Poniżej znajdziesz listę artykułów, na których oparłem swoją prezentację oraz ten (najdłuższy jak do tej pory, bo ok. 2500 słów) wpis. Jeżeli byłeś na mojej prezentacji, to serdecznie dziękuję Ci za uczestnictwo, jeżeli zaś trafiłeś tylko na ten wpis, to mam nadzieję, że sam siądziesz i potestujesz te nowe smaczki. Przepraszam, że tak długo i mam nadzieję do zobaczenia! 🙂

Bibliografia