Sytuacja kobiet w IT w 2024 roku
17.03.20207 min
Adam Kukołowicz

Adam KukołowiczCo-founderBulldogjob

Java 14 - co nowego?

Zapoznaj się z nowościami składni Javy 14 i przystępnym opisem reszty z 16 JEP-ów w tym wydaniu.

Java 14 - co nowego?

Od dzisiaj - 17.03.2020 - Java 14 jest szeroko dostępna i gotowa do wgrywania na produkcję! Do tego wydania wchodzi 16 JEP-ów (czyli JDK Enhancement Proposal). Tak wielu zmian nie widzieliśmy od czasów Javy 11. Java 14 nie jest wydaniem LTS, więc będzie wspierana zaledwie przez pół roku, do września 2020. Mimo to wprowadza kilka ciekawych rozwiązań, które z pewnością znajdą szerokie zastosowanie w codziennej pracy.

Zmiany języka Java

Kilka ostatnich wydań przyzwyczaiło nas do drobnych ulepszeń składni i tego rodzaju nowości nie zabrakło też w Javie 14.

Wyrażenie Switch w standardzie

Prace nad wprowadzeniem zmodernizowanego switch rozpoczęły się w grudniu 2017. W tym czasie mogliśmy poeksperymentować z nim w wersjach 12 i 13, a teraz staje się częścią standardowej Javy (JEP 361).

Nowy switch ma parę interesujących cech. Po pierwsze zamiast case L:, wprowadzono nową składnię case L ->. Ma to podkreślać, że wykonane zostanie tylko to co jest po prawej od ->.

Dodatkowo można teraz używać wielu etykiet w jednej linijce: case L1, L2, L3 -> jest poprawne.

Po prawej od -> można zapisywać zarówno wyrażenia, instrukcje czy też literały. To wszystko przekłada się na taki kod:

switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
    case TUESDAY                -> System.out.println(7);
    case THURSDAY, SATURDAY     -> System.out.println(8);
    case WEDNESDAY              -> System.out.println(9);
}


Poza tym switch może być również wyrażeniem, więc zwraca wartość:

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};


W tej formie musimy zapewnić, że nasz switch obsłuży wszystkie możliwe przypadki, co jest dość logiczne. Gdy jest w formie instrukcji, można obsłużyć tylko wybrane wartości wejściowe.

Dość jasne jest, co dzieje się w nowej składni, gdy mamy jedną linijkę per case. A co, gdy chcemy, by wywołać kilka linijek w nowej składni i dodatkowo zwrócić określony wynik? Należy użyć instrukcji yield, której argument zostanie zwrócony jako wartość switch.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};


Switch można też użyć z tradycyjną składnią case:  jako wyrażenia, wtedy jednak trzeba pamiętać, by w każdym miejscu użyć yield, w celu przekazania wartości.

Bloki tekstu

To druga wersja nowej funkcji, pozwalającej na wygodne tworzenie wielolinijkowych łańcuchów znaków (JEP 368 to nadal preview, więc trzeba użyć przełącznika --enable-preview, żeby użyć tego w swoim kodzie). Podstawowe możliwości opisałem już w artykule o Javie 13, więc zapraszam do zajrzenia tam, jeżeli nie wiesz o co chodzi.

W skrócie, taka składnia:

"""
line 1
line 2
line 3"""


Jest równoznaczna z:

"line 1\nline 2\nline 3"


Jedyną nowością są nowe sekwencje specjalne. Pierwsza to \<line-terminator>, która sprawia, że nowa linijka z bloku tekstu nie pojawi się w finalnym łańcuch znaków. Druga to \s, dzięki której białe znaki przed sekwencją nie znikną z końcowego rezultatu.

Rekordy

Nowa funkcja, w fazie preview. Rekordy to proste kontenery niemutowalnych danych. Motywacją jest to, że stworzenie klasy, która trzyma tylko dane wymaga o wiele większej uwagi, niż jest to konieczne. Definiowanie akcesorów czy metod takich jak equals(), hashCode(), toString() w tym przypadku to źródło frustracji i błędów. Owszem, współczesne IDE robią to wszystko za pomocą jednego kliknięcia, ale developerzy języka uważają jednak, że to też jest nadmiarowe. Stąd potrzeba rekordów.

Dobry przykład obrazujący możliwości rekordów znajdziemy w dokumentacji JEP 359:

record Range(int lo, int hi) {
  public Range {
    if (lo > hi)  /* referring here to the implicit constructor parameters */
      throw new IllegalArgumentException(String.format("(%d,%d)", lo, hi));
  }
}


Rekordy mają nazwę (Range) i opis stanu (int lo, int hi), który deklaruje składowe elementy. Opcjonalnie deklarując rekord można podać też jego ciało (czyli wszystko między {}), jednak w większości przypadków będzie można to pominąć. Rekordy automatycznie będą miały pewne właściwości:

  • Prywatne i finalne pola na dane z opisu stanu
  • Publiczne akcesory do odczytu dla wszystkich zdefiniowanych pól - o takich samych nazwach i typie jak oryginalne pole.
  • Publiczny konstruktor o sygnaturze identycznej jak opis stanu rekordu
  • Implementację equals i hashCode - dwa rekordy są równe jeżeli są tego samego typu i zawierają identyczny stan
  • toString- który wypisze komponenty rekordu wraz z nazwami


To dopiero pierwsze preview i należy się spodziewać tu więcej w kolejnych wydaniach Javy - np. API do refleksji.

Pattern matching w instanceof

Tzw. pattern matching dla instanceof to kolejna funkcja preview (JEP 305). Pattern w rozumieniu tej funkcji, to zestaw predykatu, do wykonania na obiekcie i powiązanych zmiennych, które zostaną wyciągnięte z obiektu, w przypadku, gdy predykat będzie prawdziwy. W praktyce wygląda to tak:

if (obj instanceof String s) {
    // can use s here
}


Dzieją się tu następujące rzeczy - sprawdzamy czy obj jest instancją String, jeżeli tak, to zostaje utworzone s oraz przypisujemy do niego wynik rzutowania obj na String.

Zmienna s nie jest klasyczną zmienną, s to definicja powiązanej zmiennej, która zostanie utworzona, tylko w przypadku, gdy predykat okaże się prawdą. W innym wypadku nie będzie można jej używać. Używanie zmiennej s będzie tu ograniczone tylko do bloku, w którym predykat jest prawdziwy. Finalnie usuwa to nieco boilerplate’u, gdy chcemy użyć obiektu, który testowaliśmy za pomocą instanceof. Dodatkowo są plany na połączenie pattern matchingu z nową składnią switch (i będzie można robić tak hipsterskie rzeczy jak wykonanie określonej logiki w zależności od klasy obiektu).

NullPointerException bardziej pomocny

Zespół Javy stwierdził, że wiadomość z wyjątku NullPointerException nie jest zbyt przydatna, szczególnie przypadku takim, jak:

a.b.c.d.e = 1;


Dostaniemy tu informację, że w tej linijce pojawił się NPE… ale gdzie konkretnie? Dzięki JEP 358 dowiemy się tego!

Exception in thread "main" java.lang.NullPointerException:
        Cannot read field "d" because "c" is null
    at Prog.main(Prog.java:5)


Podobnie będzie z dostępem do zagnieżdżonych tablic - też dostaniemy dokładne wskazanie na nulla.

Porządkowanie Garbage Collectorów

W ostatnich kilku wydaniach było widać intensywną pracę w obszarze garbage collectorów dostępnych w Javie. Nie inaczej jest tym razem.

Usunięto garbage collector o nazwie Concurrent Mark Sweep (JEP 363) - był on oznaczony jako przestarzały od Javy 9. W tym czasie nie znalazł się nikt kto by mógł utrzymać wsparcie dla CMS.

Jako przestarzałą oznaczono kombinację algorytmów GC ParallelScavenge i SerialOld (JEP 366). Zamiast tego można użyć algorytmu Parallel, który zachowuje się na tyle podobnie, że można go traktować jako zamiennik.

ZGC, czyli bardzo szybki garbage collector, który ma na celu odśmiecać w czasie do 10ms do tej pory był dostępny tylko na Linuxie, bo wykorzystywał pewne właściwości tej platformy. W tym wydaniu doczekał się portów na Windowsa (JEP 365) 10 i Windows Server nowszego niż wersja 1803 oraz MacOS (JEP 364).

Do G1 dodano wsparcie dla NUMA. NUMA oznacza non-uniform memory access, co oznacza, że dostęp do pamięci z różnych socketów lub rdzeni może mieć różną wydajność. G1 do tej pory nie wiedział o takim rozróżnieniu, co jest o tyle problemem, że współczesny sprzęt coraz szerzej korzysta z NUMA. Tym samym wydajność G1 była ograniczona na tego typu sprzęcie. Dzięki JEP 345 G1, podobnie jak GC Parallel, zyskuje świadomość o NUMA i wykorzystuje to przy alokacjach.

W inkubatorze

Z pozostałych nowości najciekawsze wydają się dwie inicjatywy, które trafiły do inkubatora, czyli są małymi podprojektami, których rozwój będzie śledzony w dłuższym okresie, by być może dołączyć do standardowej Javy (lub umrzeć, gdy pojawi się bardziej seksowna alternatywa).

Pierwszą z nich jest narzędzie do pakowania aplikacji Javy (JEP 343). Obecnie Java nie posiada dobrego narzędzia do pakowania aplikacji do instalatorów natywnych, dobrze obsługiwanych przez Windowsa (msi, exe), MacOS (pkg, dmg) czy Linuxa (rpm, deb). W oparciu o javapackager z JavyFX ma powstać tu właśnie takie narzędzie dla standardowej Javy, które będzie można skutecznie wywoływać również z linii poleceń w celu tworzenia instalatorów aplikacji javowych.

Druga ciekawa inicjatywa to API dostępu do obcej pamięci (JEP 370, Foreign-Memory Access API). Celem jest tu stworzenie bezpiecznego API do dostępu do pamięci znajdującej się poza stertą pamięci Javy. Taki trick przydaje się szczególnie w programach Javy, w których należy unikać niepotrzebnych cykli GC czy trzeba współdzielić pamięć z innymi procesami. Robi tak wiele bibliotek i programów w Javie. Alternatywami są ByteBuffer, Unsafe lub w ostateczności JNI. Czas pokaże jak zostanie przyjęta ta inicjatywa.

Inne JEP-y

JDK Flight Recorder doczekał się API, które pozwoli na ciągłą konsumpcję nagranych danych i ich monitoring (JEP 349). To spore ułatwienie, bo do tej pory trzeba było najpierw uruchomić, potem zebrać, następnie zakończyć nagranie, by móc je analizować. Obecnie dane mogą być pobierane w formie strumienia, konsumowanego w dowolny sposób.

Dzięki JEP 352 będzie można użyć API FileChannel by stworzyć MappedByteBuffer, które będą odwoływać się do pamięci nieulotnej. Z praktycznej strony oznacza to o wiele wydajniejszy dostęp i zapis do NVM. Do tej pory Java robiła to dość nieefektywnie używając wywołań systemowych lub JNI w tym przypadku. Nowe podejście pozwala zmniejszyć różnicę do najbardziej wydajnego w tym przypadku C.

W tym wydaniu żegnamy pack200, który służył do pakowania aplikacji Javy (JEP 367). Jako przestarzałe zostały oznaczone porty OpenJDK na Solarisa i SPARC (JEP 362) - utrzymywanie ich konsumowało zasoby, które mogą być spożytkowane lepiej.

Podsumowanie

I to by było na tyle, jeżeli chodzi o JEP-y. To wydanie przyniosło kilka naprawdę ciekawych zmian. Potwierdza się dążenie zespołu Javy do modernizacji składni - jednak nie wszelkim kosztem. Zmiany wprowadzane są rozważnie, co zobaczyliśmy na przestrzeni 3 ostatnich wydań od LTS-a. Każda modyfikacja składni najpierw ląduje jako preview, co okazło się kluczowe np. w przypadku switch - gdzie break zostało zastąpione przez yield, by uniknąć pomyłek.

Systematycznie dostajemy też ulepszenia garbage collectorów, które są kluczowe dla wydajności aplikacji JVM. Zdarzają się też miłe niespodzianki - jak poprawa komunikatu NPE.

<p>Loading...</p>