We wrześniu mieliśmy premierę Javy 9 oraz zapowiedź zmiany cyklu wydawniczego tego języka. Teraz nowe wersje będą pojawiać się co pół roku. W marcu oraz we wrześniu. Zmianie ulegnie również wersjonowanie kolejnych wydań. Na stronie Oracle kolejne wydania Javy mają oznaczenie rok.miesiąc. Tak więc za kilka miesięcy zamiast Javy 10 zobaczymy Javę 18.3. Zastanawialiście się już jakie nowości mogą trafić do tej wersji Javy?

Zmiany w 18.3

Java 18.3 zaoferuje nam oprócz bug fixów, zaledwie 12 większych nowości. W porównaniu z Java 9, gdzie lista nowości obejmowała prawie 100 pozycji, nie jest to aż tak rewolucyjne wydanie. W końcu przy półrocznym cyklu wydawniczym nie ma sensu oczekiwać zbyt wielu nowinek w języku. W tych zapowiedzianych przez OpenJDK nowościach (OpenJDK jest wzorcową dla Oracle implementacją jvm) tylko jedna, moim zdaniem, będzie miała znaczący wpływ na to, jak programujemy. Ale o tym za chwilę, najpierw zobaczmy, czym są pozostałe zapowiedziane zmiany:

  • Przez lata kod źródłowy jdk podzielony był na kilka repozytoriów. Dla Javy 9 było ich aż 9, teraz planowane jest umieszczenie wszystkiego w jednym miejscu.
  • Zmiany w G1 wprowadzające wielowątkowe odśmiecanie oraz modularność kodu GC.
  • Rozbudowa mechanizmu współdzielenia metadanych pomiędzy różnymi procesami Javy. Polega on na tym, że podczas ładowania klasy, jvm najpierw sprawdza czy
    metadane klas znajdują się w cache, a dopiero później próbuje je wczytać. Feature ten pojawił się również w jdk-8u40 jako eksperymentalny. Można go uruchomić za pomocą
    flagi -XX:+UseAppCDS. Jak podano na stronie OpenJDK mechanizm pozwoli przyspieszyć start aplikacji o 20-30% oraz zmniejszyć zużycie pamięci o kilka procent.
  • Optymalizacja zarządzania wątkami przez jvm.
  • Usunięcie narzędzia javah służącego do generowania nagłówków i kodu metod natywnych w C. Od jdk 1.8 zajmuje się tym kompilator javy, stąd też dodatkowe
    narzędzie nie jest już potrzebne.
  • Obsługa nieulotnej pamięci RAM: NV-DIMM.
    Oferuje ona wolniejszy dostęp dla danych stosu niż DRAM, ale za to korzystając z niej możemy szybszą pamięć zostawić ważniejszym procesom.
    Jako parametr jvma będzie można podać w jakiej pamięci ma być alokowany stos.
  • W jdk 9 pojawił się eksperymentalny feature - kompilacja ahead of time. Dzięki skompilowaniu kodu Javy bezpośrednio do kodu natywnego na danej platformie,
    podczas runtime nie ma potrzeby spowalniać aplikacji kompilacją JIT, która uruchamiana jest dopiero po wykonaniu dużej liczby wywołań danego fragmentu kodu
    (tzw. rozgrzanie javy) i nie daje gwarancji, że cały byte code zostanie skompilowany do C. Kompilacja AOT możliwa jest za pomocą polecenia jaotc i wykorzystuje kompilator
    o nazwie Graal. Został on napisany w Javie i w jdk 18.3 będzie można go wykorzystać nie tylko do AOT, ale również do JIT. Oczywiście są to mechanizmy, których
    nie powinniśmy dodawać jeszcze do naszych projektów, z powodu bardzo wczesnego etapu prac nad nimi.

Wisienka na torcie

Java jako język statycznie typowany zawsze wymagała podawania jakiego typu będzie zmienna. Dodatkowo często zdarza się, że informacja o typie znajduje się również w nazwie zmiennej. Jest to niedogodność względem języków dynamicznie typowanych takich jak np. javascript, gdzie zmienne deklaruje się bez podania typów. Piszemy var i na poziomie runtime określany jest typ danych, jakie są przechowywane w zmiennej. Pisząc w javie wiele osób narzeka na boilerplate jaki powstaje w tym języku. Przykładowa bardziej zaawansowana deklaracja mapy w Javie 1.6 wygląda tak:

1
2
HashMap<BigDecimal, Map<String, LinkedList<String>>> dataMap
= new HashMap<BigDecimal, HashMap<String, LinkedList<String>>>();

Wychodzi nam porządny jamnik. Java 1.7 upraszcza ten zapis wprowadzając operator diamond, dzięki któremu typ generyka zostaje określony na podstawie deklaracji zmiennej:

1
HashMap<BigDecimal, Map<String, LinkedList<String>>> dataMap = new HashMap<>();

Już jest lepiej. Ale co w przypadku kiedy chcę wyjąć z mapy jakiś element i przypisać do zmiennej? Znów trzeba podać typ elementu. I dostaję coś takiego:

1
Map<String, LinkedList<String>> value = dataMap.get(amount);

Nie mamy możliwości skrócenia takiego zapisu. Z drugiej strony, typ zmiennej mógłby być z powodzeniem określony na podstawie typu zmiennej dataMap. Twórcy javy chcą ułatwić nam życie i dodać do języka możliwość określania typu zmiennej lokalnej na podstawie tego, co do niej przypisujemy. Planowane jest wykorzystanie w tym celu słówka kluczowego var. Zamiast podawać typ zmiennej, który kompilator sam za nas określi, będzie można napisać:

1
var value = dataMap.get(amount);

Taka instrukcja wygląda dużo prościej niż ta z generykiem. Jednak podejrzewam, że może prowadzić do sytuacji kiedy trzeba będzie się chwilę zastanowić jakiego typu obiekt mamy w zmiennej. Oczywiście będą też przypadki, w których kompilator nie domyśli się za nas jakiego typu ma być ta zmienna. Przykładowo próbując przypisać lambdę reprezentującą Runnable dostaniemy błąd:

1
var runable = () -> {}; // error: cannot infer type for local variable

Deklaracje zmiennych lokalnych, gdzie moglibyśmy uprościć zapis korzystając z var to według twórców Javy ponad 90% kodu. Co ciekawe, jako alternatywę dla var podano możliwość rozbudowy wyrażenia diament, tak żeby można było wykorzystać go z lewej strony operatora przypisania. I dawałoby to taki zapis:

1
Map<> value = dataMap.get(amount);

Po co czekać skoro jest Lombok

Rozwiązanie pozwalające pomijać podawanie typów podczas deklaracji zmiennych oferuje obecnie biblioteka Lombok. Oferuje ona dwa typy:

  • val - zmienna final, której typ określany jest podczas kompilacji. Obecnie w wersji stabilnej biblioteki (od 0.10).
  • var - zmienna mutowalna, której typ określany jest podczas kompilacji. Obecnie jeszcze jako eksperymentalny feature. Jeżeli var wejdzie
    do języka w ramach JEP 286 to Lombok z pewnością usunie ją. Dostępna od wersji 1.16.12.

Jeżeli chodzi o mutowalności var w javie 18.3 - architekci nadal dyskutują. Oczekując na kolejny release javy możemy ściągnąć Lomboka i poeksperymentować z val/var, tak żeby znać wady i zalety tego podejścia, kiedy już wejdzie oficjalnie do języka. Na koniec przykład wykorzystania Lomboka:

import lombok.val;
    ...
    val map = new HashMap<Integer, String>();
    map.put(0, "zero");
    map.put(5, "five");
    
    for (val entry : map.entrySet()) {
      System.out.printf("%d: %s\n", entry.getKey(), entry.getValue());
    }
    ...