Gotowi na Valhallę?

softwaregarden.dev 3 lat temu

Wiele zmian, które obserwujemy w kolejnych wydaniach Javy (jeśli nie większość z nich), powstaje w ramach projektów grupujących / parasolowych. Często widzimy ulepszenia składni z projektu Amber, projekt Jigsaw przyniósł nam moduły, projekt Loom (uwaga, niespodzianka!) zajmuje się wątkami, projekt Panama, gerenalnie rzecz biorąc, jest o wydajnym i bezpiecznym wykorzystaniu metalu pod spodem. Spośród tych najbardziej znanych, projekt Valhalla też zaczyna do nas docierać, dzięki wydaniu JEPa-390 w ramach Javy 16.

Z mojego punktu widzenia (i wcale nie musicie się ze mną zgadzać), JEP-390 w swej naturze przypomina JEP-396, który opisałem w poprzednim wpisie. Co je od siebie odróżnia, to czas.

Jak wiemy, JPMS (Java Platform Module System), składający się z wielu JEPów i JSRów, ujrzał światło dzienne z Javą 9, bo wieloletnim przekładaniu wydania. I gdy go ujrzeliśmy, w pakiecie od razu dostaliśmy też ostrzeżenia o illegal access, czyli rzeczywista zmiana oraz ostrzeżenia o tej zmianie pojawiły się równocześnie, w tym samym wydaniu. Rzecz jasna, wciąż doświadczamy zmian w tych ostrzeżeniach (i naruszaniu zasad OOP) w Javie 16 i (najprawdopodobniej) Javie 17, jednakże przed pojawieniem się zmiany nie było o niej ostrzeżeń w żadnym wcześniejszym wydaniu, ani w javac, ani w JVMie.

W porównaniu do tego sposób publikowania Valhalli jest inny. Wpierw widzimy ostrzeżenia, co daje nam czas i możliwość na przygotowanie się do zasadniczych zmian, które Valhalla ma wprowadzić. Projekt Valhalla (i to jest spore uproszczenie) ma na celu umożliwić tworzenie value-based objects1, to jest takich obiektów, które w jakimś sensie przypominają typy prymitywne w Javie. Przykładowo, gdy mamy dużą tablicę obiektów, powiedzmy milion obiektów, czy wszystkie te obiekty rzeczywiście potrzebują swoich unikalnych nagłówków, skoro wszystkie mają dokładnie taki sam nagłówek i żadnego z nich nie wykorzystujemy do synchronizacji? Może te obiekty powinny być bardziej int-owe w swej naturze? Tak, byśmy mogli dać sobie spokój z ich tożsamością (ang. identity), z której i tak nie korzystamy, by więcej wykręcić z wydajności? Mniej więcej o to chodzi: by mieć szansę tworzenia obiektów, w których istotna jest tylko ich wartość, stąd value-based.

Wygląda na to, iż może istnieć przynajmniej kilka klas, które powinny być wykorzystywane tylko ze względu na wartości przechowywane w ich obiektach, i które powinny być traktowane bardziej jak dane prymitywne, mimo iż nie są zdefiniowane jako typy prymitywne.

W tym miejscu polecam zapoznać się z oczekiwanymi cechami value-based class.

Czym w zasadzie jest value-based class?

[Aktualizacja: Mówiłem o tym także na początku Java 16. Nowości godne uwagi.]

Aktualnie są to wszyskie klasy z adnotacją jdk.internal.@ValueBased. Przykładowo:

  • java.lang.{Byte, Short, Integer, Long, Float, Double, Boolean, Character}
  • java.util.{Optional, OptionalDouble, ...}
  • java.time.{Instant, ZonedDateTime, Duration, ...}
  • implementacje rezultatów zwracanych przez metody takie, jak List.of(), List.copyOf(), Set.of(), ..., Map.entry()

Jeśli znów dokonamy pewnego uproszczenia, to o takich klasach można powiedzieć, iż ich obiekty powinny być tworzone z wykorzystaniem metod fabrycznych (nie konstruktorów) oraz iż rezygnują one ze swej tożsamości (ang. identity), czyli możliwości porównania z wykorzystaniem == czy wykorzystania w bloku synchronized, by polegać całkowicie na równości (and. equality), czyli możliwości porównania z wykorzystaniem equals() Javie. Dzięki temu oraz dzięki byciu niezmiennymi (ang. immutable), rzeczone obiekty/instancje mogą być łatwo zastępowane kopiami (które zwracają true z equals), coś na zasadzie -XX:+UseStringDeduplication.

Rzućmy teraz okiem™ na autentyczne przykłady, o czym są te zmiany, i jak wykryć przypadki niewłaściwego użycia w czasie kompilacji i uruchomienia.

Wszystkie konstruktory są teraz @Deprecated(forRemoval = true)

Wykorzystywanie konstruktorów dla wspomnianych klas chyba nigdy nie było łatwe, ani nie było dobrym pomysłem, ale jeżeli teraz zechcemy wywołać któryś bezpośrednio w naszym kodzie, to zobaczymy nie tylko ostrzeżenie w IDE (np. w formie czerwonego przekreślenia), ale także ostrzeżenie kompilatora:

warning: [removal] Long(long) in Long has been deprecated and marked for removal private final static Long aLong = new Long(42); ^

Rozwiązanie jest dość proste: trzeba przestać wywoływać te konstruktory, zamiast tego wykorzystywać bezpośrednio metody fabrycznie, np. Long.valueOf(42L), Duration.ofSeconds(15) lub autoboxing (gdy ma on sens).

Próby synchronizacji generują ostrzeżenie typu synchronization

Przy próbach synchronizacji na obiekcie value-based class w naszym kodzie, kompilator ostrzeże nas, wykorzystując nowy typ ostrzeżenia, synchronization:

warning: [synchronization] attempt to synchronize on an instance of a value-based class synchronized (aLong) { ^

To ostrzeżenie jest bardziej skomplikowane, ponieważ niektóre narzędzia do budowania (ang. build tools) mogą jeszcze nie być świadome tego rodzaju ostrzeżenia, przez co możemy w ogóle nie uświadczyć tego ostrzeżenia. Np. wykorzystując wtyczkę Mavena do kompilacji w wersji 3.8.1, choćby jeżeli bezpośrednio włączyć wszystkie ostrzeżenia kompilatora (albo tylko <compilerArg>-Xlint:synchronization</compilerArg>), nie da się tego ostrzeżenia zobaczyć, gdyż wykorzystanie:

<plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <compilerArgs> <!--<compilerArg>-Xlint:synchronization</compilerArg>--> <compilerArg>-Xlint:all</compilerArg> </compilerArgs> </configuration> </plugin>

pokazuje tylko wcześniejsze ostrzeżenie o wykorzystaniu konstruktora:

[WARNING] (...)ValueBasedClasses.java:[28,39] Long(long) in java.lang.Long has been deprecated and marked for removal [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------

Proszę, bądźcie tego świadomi. Na szczęście Intellij IDEA oznacza takie klauzule synchronized poprawnie (najwyraźniej nie wymaga to indeksowania ;-) )

Analiza cudzych artefaktów

Często bywa tak, iż nie kompilujemy wszystkich źródeł sami, zamiast tego wykorzystujemy jakieś JARy od innych dostawców lub zespołów. Nie ma powodu do strachu, po prostu wykorzystaj jdeprscan i Robercik twym wujaszkiem! 2

$ jdeprscan goodies/target/goodies-1.0-SNAPSHOT.jar Jar file goodies/target/goodies-1.0-SNAPSHOT.jar: class ValueBasedClasses uses deprecated method java/lang/Long::<init>(J)V (forRemoval=true)

Niestety, nic nie wykrywa wywołań w rodzaju:

Long.class.getDeclaredConstructors()[0].newInstance("42");

lecz chyba powinna być jakaś granica szaleństwa ;-)

Wykrywanie synchronized w trakcie uruchomienia

Wykrywanie synchronized(valueObject) można także włączyć w czasie uruchomienia, co jest szczególnie przydatne dla artefaktów zewnętrznych (bo nie są wykrywane przez jdeprscan). By to zrobić, parametry java powinny zawierać jeden z dwóch poniższych zestawów:

java -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=1

albo

java -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=2

Różnica tkwi w sposobie, w jaki JVM zareaguje na wykrycie synchronizacji na value object. DiagnoseSyncOnValueBasedClasses=1 natychmiast przerwie uruchomienie u zobaczymy mniej więcej taką wiadomość:

# Internal Error (synchronizer.cpp:416), pid=29406, tid=29407 # fatal error: Synchronizing on object 0x000000062f2a7bb8 of klass java.lang.Long at ValueBasedClasses.main(ValueBasedClasses.java:32)

Natomiast użycie DiagnoseSyncOnValueBasedClasses=2 nie ubija procesu (program będzie dalej działał), wypisuje przy każdym wykryciu mniej więcej coś takiego:

[0,076s][info][valuebasedclasses] Synchronizing on object 0x000000062f2a7818 of klass java.lang.Long [0,076s][info][valuebasedclasses] at ValueBasedClasses.main(ValueBasedClasses.java:32) [0,076s][info][valuebasedclasses] - locked <0x000000062f2a7818> (a java.lang.Long)

Co mam zrobić, jeżeli nie widzę tych ostrzeżeń?

Najpierw polecam sprawdzić, czy te ostrzeżenia są włączone i zgłaszane przez środowiska do budowania i uruchamiania.

Później jest już chyba łatwo, wystarczy przestać wykorzystywać konstruktory value-based classes a ich obiekty w klauzulach synchronized (można wykorzystać inne obiekty jako monitory, choćby Object lock = new Object();).

Nie ma żadnego przełącznika (a przynajmniej ja nie słyszałem), który pozwoliłby wyłączyć jakoś takie ostrzeżenia w przyszłości, coś na kształt --illegal-access=permit. I choćby gdyby był, to i tak nie polecam z niego korzystać.

Jeśli Twój kod będzie wykorzystywać value-based classes w sposób niehonorowy, który powoduje takie ostrzeżenia, nigdy nie wkroczysz z podniesionym czołem do Valhalli po wieczną chwałę!


  1. Jeszcze nie wymyśliłem jak dobrze przetłumaczyć value-based classes czy value objects na polski. Chodzi o klasy i obiekty, których sens istnienia i wykorzystania opiera się na przechowywanych wartościach, stąd pewnie powinno być klasy oparte na wartościach. Gdyby to skrócić do np. “klasy wartościowe”, to niosłoby to niebezpieczny ładunek semantyczny, iż inne klasy są np. “bezwartościowe”, a tego byśmy pewnie nie chcieli. Z drugiej strony jest to tłumaczenie przydługie. Gdy już ktoś wymyśli jak to zgrabnie przetłumaczyć, by oddać sens, bez dosłownego tłumaczenia słowo po słowie, to chętnie dokonam podmiany. #ąęszcz ↩︎

  2. W oryginale po angielsku Bob’s your uncle!, bo tak ;-) ↩︎

Idź do oryginalnego materiału