Java 18: co nowego? - przegląd JEP-ów

Wydanie Javy 18 planowane jest na 2021-03-22. Spójrzmy, co nowego się w nim (prawdopodobnie) znajdzie. Zapraszam na szybki przegląd planowanych zmian i nowości. Zaparzcie sobie kubek pysznek kawy ☕: i zaczynamy!
Najlepszym miejscem do śledzenia tego, jakie zmiany będą włączone do Javy, jest strona openjdk.java.net podsumowująca określone wydanie.
Dla Javy 18 będzie to strona https://openjdk.java.net/projects/jdk/18/
Wciąż jescze trwa proces migracji wielu projektów na Javę 17.
Programiści, którzy (często boleśnie) przekonali się, że nie warto pozostawać z tyłu, starają się nadganiać dość jednak wysokie - jak na standardy firmowe - tempo, w jakim wydawane są kolejne wersje Javy.
Czasami nadganianie jest jedynie mentalne - jak w przypadku tych nieszczęśników, którzy wciąż używają Javy 7… wiem, wiem: takie mamy teraz uwarunkowania.
W tym wpisie przedstawię niektóre JEPy planowane w wydaniu 18 platformy openjdk.
Data “powszechnej dostępności” (general availability) dla JDK 18 została ustalona na 2022-03-22, więc całkiem możliwe, że lista JEPów, które ostatecznie wylądują w tym wydaniu, będzie się od poniższej listy znacznie różnić.
Według dzisiejszych planów istnieje osiem JEPów planowanych w Javie 18:
- 400: utf-8 by default: domyślne kodowanie do UTF-8
- 408: simple web server: prosty serwer webowy
- 413: code snippets in java api documentation: fragmenty kodu w dokumentacji API
- 416: reimplement core reflection with method handles: implementacja refleksji przy pomocy uchwytów metod
- 417: vector api (third incubator): API dla operacji wektorowych
- 418: internet-address resolution spi: interfejs do dostarczania usług adresacji internetowej
- 419: foreign function & memory api (second incubator): API do wywoływania funkcji zewnętrznych i dostępu do pamięci
- 420: pattern matching for switch (second preview): dopasowanie wzorca w instrukcji/wyrażeniu switch
Przyjrzyjmy się niektórym z nich trochę bliżej
Opisy JEP-ów, które znajdziecie poniżej, są jedynie moimi prywatnymi notatkami. Same propozycj (JEPy) będą prawdopodobnie zmieniać się w kolejnych wersjach dokumentów, podobnie jak prezentowane niżej API.
Niektóre zagadnienia, o których piszę poniżej, rozumiem jedynie częściowo, więc mogę w ich opisie popełnić merytoryczne błędy, za które potencjalnych czytelników z góry przepraszam.
Mam nadzieję, że - mimo możliwych błędów - ten artykuł stanie się inspiracją i zachętą do własnych poszukiwań.
Potrzeba ustalonego kodowania
Ten JEP został stworzony już cztery lata temu. Jego cel to poprawa przewidywalności programów napisanych w Javie w związku z użyciem zestawów znaków.
W przypadkach, gdy nie ma możliwości podania kodowania jako parametru wywołania funkcji z API, wówczas powinno być jednoznacznie ustalone, jakie kodowanie będzie użyte.
Na przykład, odniesienie do metody lub konstruktora w strumieniach lambda (::) możliwe jest tylko wtedy, gdy metoda/konstruktor są jednoargumentowe, więc w takich przypadkach programista nie może użyc standardowego parametru charset, choćby nawet istniała taka wersja metody/konstruktora.
Obecnie stosowane kodowanie jest zależne od platformy.
Według tego JEP-a, jeśli zawsze będzie używane ustalone z góry kodowanie, to będzie można w końcu pisać przenośny kod (między systemami mającymi różne kodowania domyślne), a dzięki temu osiągnąć przewidywalny rezultat (np. przetwarzania plików).
UTF-8 jako kodowanie domyślne
Wybrane zostało kodowanie UTF-8. Jest szeroko rozpowszechnione w sieci, jest używane w standardzie XML oraz w porządnych systemach operacyjnych takich jak Linux. Będzie więc domyślnie używane również w całym API biblioteki standardowej.
Domyślne kodowanie bieżącego JDK można sprawdzić używając parametru -XshowSettings:properties
przy opcji java -version
i szukając właściwości file.encoding
:
|
|
Obecnie istnieje wiele API które używają kodowania systemowego:
- w pakiecie
java.io
: konstruktory InputStreamReader, FileReader, OutputStreamWriter, FileWriter, i PrintStream - w pakiecie
java.util
: Formatter i Scanner - w pakiecie
java.net
: URLEncoder i URLDecoder (w metodach oznaczonych jako deprecated)
Zmiana
Najistotniejsza zmiana jest następująca: specyfikacja Charset.defaultCharset() będzie brzmiała zawierała zapis, że domyślnym kodowaniem jest UTF-8 chyba że sostało to skonfigurowane inaczej w zależny od implementacji sposób.
Aby użyć czegoś innego niż UTF-8, użytkownicy będą mogli ustawić wartość właściwości file.encoding
na:
- COMPAT - która włączy zachowanie sprzed Javy 18
- UTF-8 - która nic nie zmieni
- inną wartosć - zachowanie nie jest w takim wypadku określone
Kodowanie System.out
i System.err
będzie w dalszym ciągu określane przy pomocy Console.charset()
.
Prosty serwer webowy
Celem JEPa 408 jest:
Dostarczenie narzędzia wiersza poleceń pozwalającego na uruchimienie minimalnego serwera webowego, który jedynie udostępnia statyczne pliki. Nie jest dostępna funkcjonalność CGI ani funkcjonalność serwletów. Narzędzie to będzie użyteczne w przypadku prototypowania, kodowania ad-hoc, testowania, a szczególnie w kontekstach edukacyjnych.
Serwer w wierszu poleceń
Ten insteresujący JEP dostarcza Javowego odpowiednika istniejącego w Pythonie
|
|
Serwer Javowy będzie uruchamiany dość podobnie:
|
|
co uruchomi serwer na domyśnym porcie 8000.
Można również podać inny port:
|
|
albo zbindować się do wszystkich interfejsów (opcja -b
- bind):
|
|
Domyślnie serwer będzie dostarczał pliki z bieżącego katalogu roboczego. Katalog ten można nadpisać używając opcji -p
:
|
|
Opcje wiersza poleceń
Opcja -h
wyświetla prostą wiadomość (pomoc), która zawiera listę wsztystkich dostęonych opcji:
|
|
Notatki
- wspierany jest jedynie protokół HTTP (HTTPS nie jest wspierany)
- typy mime są ustalane autoatycznie (pliki .html są serwowane jako ext/html a pliki .java jako text/plain) - może to zostać zmienione przy użyciu API
java.net.URLConnection::getFileNameMap
- każde żądanie jest logowane na konsolę (domyślnie serwer zachowuje się tak, jakby w wierszu poleceń przekazana była opcja
-o info
; aby wyłączyć logowanie można użyć opcji-o none
; aby włączyć pełne logowanie - opcji-o verbose
) - serwer można “zanurzyć” w innych programach przy użyciu nowego API:
SimpleFileServer
,HttpHandler
iRequest
(prawdopodobnie w pakiecie com.sun.net.httpserver)
SimpleHttpServer i nowe API
Do programowego utworznenia serwera służy nowa klasa SimpleHttpServer
, która pozwala również zdefiniować handler oraz filter:
|
|
Utworzenie serwera w kodzie
W taki sposób można stworzyć i uruchomić prosty serwer plików:
|
|
Ten serwer:
- dostarcza pliki z katalogu /my/data/dir
- loguje żądania na poziomie verbose
- nasłuchuje na połączenia na porcie 8080
Są to więc parametry, które odpowiadają parametrom wiersza poleceń, odpowiednio -d
, -o
, -b
.
Inną możliwością jest użycie statycznej metody create
która została dodana do dwóch istniejących wcześniej klas: HttpServer
and HttpsServer
.
|
|
Można jej użyć do utworzenia serwera i dostosowania go do swoich potrzeb.
Dostosowanie SimpleHttpServer
Co można dostosować? Na przykład można użyć różnych handlerów (funkcji obsługi) dla różnych kontekstów, przy czym do utworzenia handlera można użyć funkcji z nowego API SimpleFileServer
:
Jak użyć dwóch handlerów do dwóch konstekstów?
Proszę spojrzeć:
- kontest “/store/” jest obsługiwany przez zarejestrowany handler
SomePutHandler
który obsługuje żądaniaPUT
- kontekst “/browse/” jest obsługiwany przez FileHandler utworzony przy użyciu nowego API
SimpleFileServer.createFileHandler()
, który będzie udostępniał pliki z katalogu “/my/data/dir”:
|
|
Jak utworzyć filtr strumienia wyjściowego?
Oto jak utworzyć serwer posiadający filtr strumienia wyjściowego.
- najpierw tworzony jest filtr, który obejmuje standardowe wyjście procesu serwera (System.out)
- taki filtr przekazany funkcji
create
spowoduje - jak sądzę - że SomePutHandler użyje standardowego wyjścia procesu obsługującego serwer jako strumienia, do którego będzie pisał odpowiedź - wobec czego każde żądanie PUT spowoduje wypisanie danych zapisanych do strumienia odpowiedzi przez SomePutHanlder na standardowe wyjście programu
|
|
Rozszerzenie obsługi żądań
Nowa klasa HttpHandlers
posiada dwie metody statyczne służące do utworzenia funkcji obsługi żądań:
|
|
Klasa abstrakcyjna Filter
pozwala utworzyć filtr zmieniający lub przystosowujący istniejące żądanie:
|
|
Może być ona użyty w następujący sposób:
HttpHandlers.handleOrElse
pozwala oddelegować obsługę żądania do inego handlera na podstawie stanu żądaniaHttpHandlers.of
do utworzenia handlera który zawsze odpowieada w taki sam sposóbRequest.with()
do zmiany bądź dodania nagłówka do każdego z nadchodzących żądań
Request jako niemodyfikowalny widok obiektu HttpExchange
Interesujące jest to, że filtry używają klasy HttpExchange, która reprezentuje pełny, mutowalny stan żądania, jednak nowa metoda klasy Filter
: adaptRequest
używa ograniczonego widoku stanu. Ten widok jest reprezentowany przez interfejs Request
:
|
|
Jest to jeszcze jeden argument w dyskusji o wyższości bytów niemutowalnych nad mutowalnymi - nowe API stara się używać minimalnych interfejsów oferujących jedynie operacje niezmieniające stanu. Warto zauważyć, że wiele ostatich (i tych trochę dawniejszych) zmian w bibliotece standardowej zostało zaprojektowanych tak, aby oferować interfejsy bądź klasy operujące na niemutowalnych obiektach (na przykład java.time
) oraz rozszerzać je przez dodawanie metod zwracających obiekty niezmienialne (np. nowe API w kolekcjach Javy).
Przykład konfiguracji serwera
Oto kod, który tworzy serwer dostosowany do pewnych określonych potrzeb. To jego możliwości:
- przekazuje obsługę żądań do różnych obiektów:
SomePutHandler
lubSomeHandler
w zależności od typu metody HTTP użytej w żądaniu; jeśli metodą jest PUT, wywołany zostanieSomePutHandler
- dodaje nagłówek Foo z waartością Bar
- nasłuchuje na porcie 8080
- odrzuci nowe połączenia przychodzące, jeśli w danej chwili jest już 10 połączeń aktywnych
- będzie działał w kontekście “/”
|
|
Fragmenty kodu w dokumentacji
413: code snippets in java api documentation
Przejdźmy do kolejnego JEP-a.
Pisanie porządnej, użytecznej i mądrej dokumentacji do kodu wymaga sporo umiejętności i jest znacznie łatwiejsze z dobrymi narzędziami. Standardowym narzędziem w Javie jest javadoc
używający domyślnie docletu Standard Doclet
.
Writing solid, usable and informative code documentation requires both skills and tools. The standard tool is javadoc which by default uses Standard Doclet.
Czym jets javadoc i doclet
Doclet jest programem napisanym w Javie który używa Javadoc API do tego, aby przeanalizować kod w Javie i wygenerować dokumenty HTML zawierające opisy kodu znajdujące się w komentarzach.
Doclet API (zwane też javadoc API) pozwala na utworzenie klasy pochodnej po Doclet
i użycie jej nazwy jako paramertru dla opcji -doclet
podczas wywołania narzędzia javadoc
:
|
|
To tylko szybkie przypomnienie, czym jest doclet. Oczywiście, programiści zwykle nie są zainteresowani pisaniem docletow. Chcą napisać kod, opatrzyć go komentarzami i wygenerować dokumentację w HTML-u (co zwyle odbywa się jako jeden z kroków procesu budowania kodu).
Używają po prostu polecenia javadoc ze standardowym, domyślnym docletem i piszą komentarze używając tagów (w postaci @tag) oznaczajacych pewne semantycznie fragmenty.
Tag {@snippet …}
Java 18 będzie zawierać nowy tag javadocowy: - @snippet - który uprości dodawanie fragmentów kodu źródłowego do komentarzy dokumentujących użycie API.
W obecnym podejściu trzeba przykładowy kod umieścić wewnątrz tagów pre i oznaczyć jako @code: <pre> {@code ….} </pre>, jednak to podejście ma kilka istotnych wad:
- narzędzia nie mogą w niezawodny sposób zastosować kolorowania składni, ponieważ nie ma sposobu na poinformowanie ich o tym, jakiego rodzaju kod znajduje się w bloku @code
- w bloku nie można używać komentarzy /* … */
- fragmenty kodu nie mogą używać znaczników HTML
- fragmenty kodu nie mogą używać komentarzy zawierających tagi linkujące do innych miejsc w API
Nowy tag @snippet
:
- może definiować snippet wewnętrzny (zawarty bezpośrednio w komentarzu w kodzie źródłowym) bądź snippet zewnętrzny (zapisany w innym pliku)
- może definiować pary klucz-wartość, zwane atrybutami:
id
definiuje identyfikator snippeta i umożliwia utworzenie odnośnikalang
określa język programowania użyty w snippeciefile
określa położenie zewnętrznego pliku ze snippetemregion
deklaruje nazwę regionu, który zostanie zdefiniowany przy użyciu tagów@start
i@end
w komentarzach wewnątrz snippeta
- może zawierać kod w języku Java, zawartość plików .properties, kod w innych językach programowania bądź zwykły tekst
- wewnętrzny snippet może zawierać tagi znacznikowe wskazujące określone regiony bądź wiersze i zawierające instrukcje, jak te regiony/wiersze zaprezentować
Dwa ograniczenia, które dotyczą wszystkich tagów javadocowych, dotyczą również nowego taga @snippet:
- wewnętrzny tag nie może używać komentarzy /* … */ ponieważ komentarz zamykający zamknie zawierający go początek komentarza dokumentującego
- tag wewnętrzny musi zawierać zbalansowane pary nawiasów klamrowych (po to, aby poprawnie określić początek i koniec bloku @snippet)
Przykład wewnętrznego @snippet-a
|
|
Przykład zewnętrznego @snippet-a z definicją regionu
Snippet zewnętrzny (tj. tag @snippet wskazujący na zewnętrzne źródło kodu, który powinien znaleźć się w komentarzu) zawiera:
- atrybut
file
wskazujący nazwę pliku, w którym znajduje się przykład - atrybut
region
deklarujący region o nazwieexample
który wraz z tagami@start
i@end
oznacza granice fragmentu kodu, który ma zostać włączony jako zawartość snippetu w javadocu
|
|
gdzie ShowOptional.java
jest plikiem zawierającym następującą treść:
|
|
Oznaczenie/kolorowanie składni
Do oznaczania składni można użyć tagu @highlight
, po którym mogą się pojawić argumenty:
// @highlight substring="println"
- oznacza wskazany podciąg// @highlight region regex = "\barg\b"
po którym następuje@end
- oznacza region, w którym oznaczanie składni ma być zastosowane dla napisów pasujących do wyrażenia regularnegotype
to parmetr, który może przyjmować wartościbold
,italic
lubhighlighted
; zostanie on przekształcony w klasę css o tej samej nazwie, a jej definicję moża umieścić w odpowiednim arkuszu stylów (systemowym bądź zdefiniowanym przez użytkownika)@replace regex='".*"' replacement="..."
- zamienia pasujące do wyrażenia regularnego napisy na wskazany zamiennik (tu: trzykropek)
Tworzenie odnośników
Snippet może również zawierać odnośniki do zewnętrznej dokumentacji. Na przykład poniższy kod wygeneruje javadoc z odnośnikiem do dokumentacji System.out
:
|
|
Używanie snippetów z właściwościami
Snippety mogą również zawierać fragmenty plików .properties z właściwościami:
|
|
Sprawdzanie poprawności
Autorzy propozycji JEP obiecują rozszerzenie API drzewa kompilacji , które uwzględni tag @snippet. Dzięki temo możliwe będzie stworzenie osobnych narzędzi, które sprawdzą poprawność snippetów:
- zewnętrzne snippety będą mogły być kompilowane (przy zachowaniu pewnego określonego kontekstu, tj. classpatha czy modulepatha)
- wewnętrzne snippety będą mogły być opakowane kodem w taki sposób, aby tworzyły poprawną jednostkę kompilacji, czyli również będa mogły być skompilowane
Wydaje się więc, że począwszy od Javy 18 dokumentacja stanie się jeszcze bardziej czytelna, szczególnie w części pokazującej przykładowe użycie opisywanego API.
Ponowna implementacja refleksji
416: reimplement core reflection with method handles
Ten JEP opisuje wewnętrzne zmiany w java.lang.reflect
oraz java.lang.invoke
, które nie mają wpływu na API.
Ich celem jest użycie uchwytów metod (Method Handles) jako mechnizmu do zaimplementowania refleksji. Obecnie istnieją jeszcze dwa sposoby implementacji reflekcji:
- wywołanie kodu natywnego w HotSpot VM (tylko na początku wykonania programu po to, aby skrócić czas uruchomienia) - ten sposób implementacji refleksji będzie wciąż obecny na wczesnych etapach działaniamaszyny wirtualnej
- drugi sposób to dynamiczne generowanie bajtkodu dla metod takich jak Method::invoke, Constructor::newInstance, Field::get oraz Field::set - zostanie on zastapiony użyciem
java.lang.invoke
API wprowadzonym w Javia 7
Zgodność
Dotychczasowa implementacja będzie wciąż dostępna pod flagą:
-Djdk.reflect.useDirectMethodHandle=false
API wektorowe
417: vector api (third incubator)
Ten JEP (a właściwie jego implementacja) będzie po raz trzeci na etapie “incubator”, co oznacza, że wciąż nie jest to API, które można uznać za dojrzałe ani stabilne.
Pod wpływem otrzymanych od użytkowników informacji zwrotnych zostały zaimplemetowane kolejne poprawki i rozszerzenia.
Głównym celem tego JEP-a jest dostarczenie nowego API wektorowego, które jest niezależne od platformy. W czasie działania kod implementujący to API powinien w rozsądny sposón skompilować się do optymalnego kodu maszynowego, to jest takiego, który używa właściwych dla architektury procesora instrukcji wektorowych.
“API wektorowe” posiada dwie implenentacje:
- czystą implementację w Javie (“funkjonalną ale nie optymalną”)
- implementację operacji wektorowych w kompilatorze HotSpot C2, w których przetwarzanie odbywa się na odpowiednich rejestrach wektorowych przy użyciu stosownych wektorowych insktrukcji CPU
Do reprezentacji wektora zostanie użyty typ VectorVector
i jego podklasy są klasami-wartościami (value classes), przy czym po wdrożeniu projektu Valhalla typy podstawowe będą mogły być rozszerzone na klasy podstawowe.
Dokumentacja JEP-a wspomina, że obecnie będzie wspierany jedynie Linux i Windows; MacOS będzie musiał jeszcze poczekać.
SPI do uzyskiwania adresów internetowych
418: internet-address resolution spi
Ten JEP przygotowuje interfejs dla dostarczycieli usług (service provider interface), którzy chcą implementować uzyskiwanie nazw hostów oraz adresów. Dzięki temu java.net.InetAdress będzie mógł używać innych sposobów uzyskiwania adresów niż domyślny, który jest bardzo prosty.
Domyślnie bowiem uzyskiwanie adresów odbywa się na podsawie pliku hosts oraz DNS. Jednak:
- obecnie uzyskiwanie adresu jest wywołaniem blokującym bieżący wątek (z podowu blokującego wywołania systemowego); jest to problematyczne dla projektu Loom, który chce używać wątków wirtualnych i wobec tego chce mieć możliwość uzyskiwania adresów w sposób nieblokujący (tak, aby bieżący wątek mógł - podczas oczekiwania na wywołanie systemowe - zacząć obsługiwać inne wątki wirtualne)
- istnieją inne protokoły, które mmogą być obsługiwane (takie jak TLS czy HTTPS)
- frameworki będą miały bardzo precyzyjną kontrolę nad uzyskiwaniem adresów
- uprości się prototypowanie i testowanie, ponieważ będzie można, na przykład, napisać zaślepkę (“zamokować”) udającą pewien komponent sieciowy używający API InetAddress
API do wywołań zewnętrznych i dostępu do pamięci
419: foreign function & memory api (second incubator)
Ten JEP jest w drugiej fazie inkubacji. Skupia się na dostarczeniu nowego API, przy pomocy którego programy w Javie będą mogły używać kodu i danych spoza runtime-a javowego.
API pozwoli kodowi klienckiemu w bibliotekach i aplikacjach wykonywać następujące operacje (w nawiasach podano nowe klasy w Javie, które definiują FFM - Foreign Funciton & Memory - API):
- przydzielić zewnętrzną pamięć (MemorySegment, MemoryAddress, and SegmentAllocator)
- uzyskać dostęp i zmieniać tę pamięć (MemoryLayout, VarHandle)
- zarządzać cyklem życia zewnętrznych zasobów (ResourceScope)
- wywoływac zewnętrzne funkcje (SymbolLookup, CLinker, and NativeSymbol)
Dopasowanie wzorca w switch
420: pattern matching for switch (second preview)
To zmiana będąca drugim preview i w porównaniu z pierwszym preview z JEP-a 406 zawiera pewne małe rozszerzenia:
- stała w labelce
case
musi się znaleźć przed wzorcem ze strażnikiem (warunkiem) tego samego typu: przykład - bardziej precyzyjne sprawdzanie “wyczerpywalności” wzorców w przypadku hierarchii klas “zaplombowanych” (sealed): przykład
Podsumowanie
Większość planowanych w wydaniu 18 Javy zmian to rozszserzenia istniejącego kodu. Tylko trzy pierwsze JEP-y w powyższej liście wprowadzają coś nowego z perspektywy przeciętnego programisty aplikacji. Są to:
-
UTF-8 jako domyślne kodowanie
-
snippety kodu jako roszerzenie dokumentacji
-
prosty http server do celów testowania
To na tych JEP-ach skupiłam się w tym wpisie, przede wszystkim dlatego, że wydaje mi się, że je rozumiem 😄
Inne, takie jak API wektorowe czu FFI - wydają mi się bardzo złożone, a ja nie czuję się ekspertem w żadnej z tych dziedzin. W zasadzie rozumiem tylko podstwowe przypadki ich użycia, przy czym rozumiem jedynie teoretycznie…
Przykładowo: nigdy nie użyłam w swoim kodzie JNI a więc pewnie nie docenię abstrakcji i użyteczności FFM API. Podobnie też nigdy nie tworzyłam potoków przetwarzających wektorowo dane obrazu (choć wyobrażam sobie, że przydałoby się tu API wektorowe, podobnie jak do przetwarzania sygnałów w sztucznych sieciach neuronowych).
Patrzę na ewolucję JEP-ów i widzę, jak platforma Javy (choć raczej platforma JVM) zmienia się i rozrasta na wiele nowych obszarów. Pozwala mi to czuć miłe ciepełko i otuchę, że przecież Java is not dead a JVM to platforma, przy której chcę zostać jako programista i w którą warto - jak sądzę - inwestować swój czas i energię.
Podziękowania
Ten wpis jest częścią serii java.
- 2021-09-12 - Java 18: co nowego? - przegląd JEP-ów
- 2021-21-09 - Java 17 - RandomGenerator i spółka
- 2021-15-09 - Java 17 - co nowego?
- 2021-04-03 - Java 15 - czym są sealed classes?
- 2021-26-02 - Java 13 i 14: Bloki tekstowe i rekordy
- 2021-24-02 - Java 12 - wyrażenie switch (preview feature)
- 2021-23-02 - Java 11 - HTTP Client i uruchamianie jednoplikowych programów
- 2021-18-02 - Java 9 - co to jest JShell i dlaczego warto używać REPL-a w Javie
- 2021-12-02 - Java 10 - var, nowe metody w Optional, kolekcje "unmodifiable"
- 2021-11-02 - Java 9 - nowości w bibliotece
- 2021-10-02 - Java 9: czy mogę stworzyć z mojej aplikacji binarkę?
- 2021-10-02 - Java 9: praktyczny przykład - trzy moduły
- 2021-08-02 - Java 8: praktyczny przykład - przewidywanie kolejnej daty w serii