To kolejny artykuł opisujący zmiany języka Java pomiędzy wersjami LTS (Long term support). W poprzednim opisywałem zmiany pomiędzy Javą 8 i Javą 11. W tym artykule opiszę najważniejsze zmiany, które zaszły od wprowadzenia Javy 11, aż do ostatnio wydanej wersji, czyli Javy 17.
Java 12
Wyrażenia switch (Switch Expressions)
Jedną z głównych zmian w języku w wersji 12 były wyrażenia switch (Switch Expressions), w tej wersji wchodziły jako zapowiedź, ale ostatecznie miały swoją premierę w Javie 14.
Normalny switch:
switch (month) { case 1: System.out.println("Styczeń"); break; case 2: System.out.println("Luty"); break; case 3: System.out.println("Marzec"); break; }
Java 12, switch expression:
switch (month) { case 1 -> System.out.println("Styczeń"); case 2 -> System.out.println("Luty"); case 3 -> System.out.println("Marzec"); }
W Javie 13 gdzie miała miejsce, druga zapowiedź Switch expression, dodano możliwość zwracania wartości poprzez słowo kluczowe yield
(to taki break
z możliwością zwrócenia wyniku).
Wyrażenia switch były przymiarką do Pattern Matching w Javie, który powoli był wprowadzany w kolejnych wersjach.
Teeing Collector
Poza switch expression było jeszcze kilka drobnych zmian w api. Dodano nowy kolektor w klasie Collectors
, jest to teeing
kolektor, który przyjmuje jako parametry dwa kolektory i funkcję (BiFunction) i działa on mniej więcej tak:
double average = Stream.of(1, 2, 3, 4, 5, 6, 7) .collect( teeing( summingDouble(i -> i), counting(), (sum, n) -> sum / n ) );
CompactNumberFormat
Została dodana także nowa klasa formatująca CompactNumberFormat
, która zapisuje duże liczby w skróconym formacie np. 1 000 000 zamienia na 1M.
Java 13
Text blocks
W tej wersji miała też miejsce druga zapowiedź switch expression, które opisałem wyżej. A kolejną funkcjonalnością, która miała swoją pierwszą zapowiedź były bloki tekstu (Text blocks), które ostatecznie została włączona do Javy w wersji 15. Bloki tekstu to inaczej wielolinijkowe stringi, z tą różnicą, że teraz można je zapisać w jednym bloku, bez konieczności użycia konkatencji i znaków nowej linii (\n
).
Stara wersja:
String html = "<html>\n" + " <body>\n" + " <p>Hello, world</p>\n" + " </body>\n" + "</html>\n";
W Java 13:
String html = """ <html> <body> <p>Hello, world</p> </body> </html> """;
Dodane zostały także trzy metody do klasy String
związane właśnie z tą funkcjonalnością. Są to metody:
- formatted() – formatuje stringa używając tego samego stringa jako formatu, jest to ekwiwalent dla wywołania
format(this, args)
- stripIndent() – usuwa nadmiarowe spacej z bloku
- translateEscapes() – zwraca stringa ze znakami eskejpującymi zakodowanymi w Unicode
Java 14
Klasy Record
W tej wersji swoje zapowiedzi miały Rekordy, które ostatecznie weszły w wersji 16. Rekordy to nowy typ klas, dzięki którym zmniejszy się ilość kodu szablonowego (boilerplate code). Rekordy posiadają bowiem niejawne metody takie jak gettery, konstruktory, equals, hashcode, toString. W rekordach nie musimy ich implementować, a możemy ich używać.
Rekordy omówiłem w jednym z moich filmów:
Pomocne komunikaty dla NullPointerException (Helpful NullPointerExceptions)
Funkcjonalność ta pomaga wyświetlić bardziej szczegółowe komunikaty błędu w przypadku wyjątku NullPointerException.
Żeby włączyć rozszerzone komunikaty, musimy ustawić JVM odpowiednią flagę:
-XX:+ShowCodeDetailsInExceptionMessages
Wtedy np. dla takiego wywołania a.b.c.d = 100;
gdy a.b
jest nullem dostaniemy:
Exception in thread "main" java.lang.NullPointerException: Cannot read field "c" because "a.b" is null at App.main(App.java:5)
Pattern Matching for instanceof
Kolejna zmiana w tej wersji to zapowiedź Pattern Matchingu dla instanceof, który wszedł ostatecznie do Javy w wersji 16. Funkcjonalność ta skraca trochę zapis sprawdzania typów poprzez instanceof. Do tej pory wyglądało to mniej więcej tak:
if (obj instanceof String) { String string = (String) obj; System.out.println(string); }
Wewnątrz bloku if, musieliśmy deklarować zmienną dla sprawdzanego obiektu, żeby z rzutować go do sprawdzanego typu. Trochę było to bez sensu, ponieważ już sprawdziliśmy, że to jest właśnie ten typ, więc rzutowanie tutaj wydaje się bardzo nadmiarowe.
Z Pattern Matchingiem jest to trochę prostsze:
if (obj instanceof String string) { System.out.println(string); } else { // nie możesz użyć tutaj zmiennej string }
Java 15
Zapieczętowane klasy (Sealed Classes)
W tej wersji Javy swoją pierwszą zapowiedź miały zapieczętowane klasy (Sealed Classes), które ostatecznie weszły do języka w wersji 17. Zapieczętowane klasy to klasy, które ograniczają możliwość dziedziczenia po sobie do tylko wylistowanych klas. Żeby odpowiednio wylistować klasy, które mogą dziedziczyć po danej klasie, musimy użyć słowa kluczowego permits
.
public abstract sealed class Animal permits Cat, Dog, Elephant {...}
I po takiej klasie mogą dziedziczyć tylko wymienione trzy klasy: Cat
, Dog
, Elephant
.
Java 16
W tej wersji właściwie nie było, żadnych nowych funkcjonalności. Na stałe weszły do Javy wcześniej zapowiadane funkcjonalności takie jak: Rekordy i Pattern Matching dla Instanceof. Kolejną zapowiedź miały klasy zapieczętowane.
Ciekawa zmiana miała miejsce w strumieniach. Dodano metodę .toList()
, dzięki czemu przy użyciu strumieni nie trzeba już robić .collect(Collectors.toList())
, tylko wystarczy .toList()
.
Java 17
W tej wersji do języka weszły zapieczętowane klasy, które opisałem już wcześniej w tym artykule, a wszystkie istotne zmiany w Javie 17 opisałem w poprzednim artykule: Java 17 LTS.
Pattern Matching dla instrukcji switch
Z nowych rzeczy pojawiła się pierwsza zapowiedź Pattern Matching dla instrukcji switch, co prawda jest preview, ale jeśli bardzo chcemy używać Pattern Matchingu w switchach to możemy sobie go włączyć w Javie 17 poprzez uruchomienie Javy z parametrem --enable-preview
. I w tej wersji Pattern Matching pozwala na używanie wyrażeń warunkowych w instrukcji switch. Wygląda to mniej więcej tak:
switch (s) { case Triangle t && (t.calculateArea() > 100) -> System.out.println("Large triangle"); case Triangle t -> System.out.println("Small triangle"); default -> System.out.println("Non-triangle"); }
Możemy także używać różnych typów jako wartości w case’ach:
String formatted = switch (o) { case Integer i -> String.format("int %d", i); case Long l -> String.format("long %d", l); case Double d -> String.format("double %f", d); case String s -> String.format("String %s", s); default -> o.toString(); };
A także nulla:
switch (s) { case null -> System.out.println("Oops"); case "Foo", "Bar" -> System.out.println("Great"); default -> System.out.println("Ok"); }
Podsumowanie
Całkiem sporo nowych wersji Javy ukazało się od Ostatniego LTSa, bo było ich aż 6, ale tak naprawdę zmian w języku jest nie wiele. Najistotniejsze to moim zdaniem rekordy. Rekordy w Javie pozwolą ograniczyć trochę uciążliwe generowanie getterów, konstruktorów itd., więc jest to duża zmiana na plus.
Chociaż w tej kwestii, od kilku już lat można używać Lomboka, i w ten sposób skracać sobie generowanie dodatkowych metod. Ale trzeba pamiętać, że Lombok nie jest natywną konstrukcją języka, a procesorem adnotacji, przez co może sprawiać pewnego rodzaju problemy. I tak jak ktoś kiedy zauważył pod jednym z moim artykułem, Lombok do działania wykorzystuje pewien hak związany z kompilatorem, co może być w przyszłości problematyczne.
Kolejna z mniej istotnych, ale przydatnych funkcjonalności to bloki tekstu, dzięki którym będzie można trochę łatwiej obchodzić się z wielolinijkowymi stringami.
Natomiast dla developerów, którzy trochę programowali w językach funkcyjnych, bardziej przydatne będzie pewnie wprowadzenie Pattern Matchingu w instrukcjach switch. Do tej pory instrukcje switch były raczej sporadycznie używane w Javie, być może po tych zmianach, to się trochę zmieni.
Źródło:
Cześć!
Dzięki za przedstawienie historii co działo się w Javie od 11 wersji. Najgorsze jest to, że większość projektów cały czas chodzi na „wygrzanej” 8 i nie można korzystać w nich z nowych elementów API. Obecnie jedyne co mogę zrobić to w swoich „Pet Projects” korzystać z dobrodziejstw nowych wersji.
Pozdrawiam!
Dzięki Cezary. Rozumiem jak to jest. Też kiedyś miałem takie problemy. Jedyne co można z tym zrobić to migrować. A jak nie zależy to od Ciebie to próbować przekonywać tych, od których to zależy. W większości przypadków da się to zrobić bez problemów, jedyny problem, to najczęściej to, że się komuś nie chce. A korzyści jest całkiem sporo, nowe funkcjonalności języku to tylko część. Są jeszcze poprawki wydajnościowe, nowe funkcje w JVM, wiele naprawionych bugów w JVM itd.
Dobre zestawienie, przystępnie wytłumaczone.
Trochę szkoda że piszesz o zapowiedziach zmian, a nie tylko co faktycznie weszło.
Trochę utrudnia to połapać się co kiedy faktycznie nowego weszło.