Do końca obecnego roku otrzymałem zadanie od firmy, aby uzyskać tytuł Oracle Certified Professional Java 8. W obecnej chwili można powiedzieć, że jestem na półmetku celu z racji tego, że zdałem egzamin OCA 🥇. Dodatkowo już teraz firma zapowiedziała, że trzeba będzie zaliczyć w przyszłym roku test certyfikacyjny z Javy 11. Z tego powodu chciałem w tym wpisie przejść po najważniejszych funkcjonalnościach, które dodał Oracle na przestrzeni tych trzech wersji. Zapraszam serdecznie do lektury!

Java 8 a Java 11

Java 8 jak i 11 są tzw. LTS (Long Term Support), czyli posiadają długie wsparcie. Pierwsza z nich miała swoją premierę dosyć dawno, bo w marcu 2014r. natomiast druga pojawiła się we wrześniu 2018r. Java 8 uznana została za wersję przełomową, natomiast jej następniki wprowadzały tylko niewielkie modyfikacje. Jednak, aby można było się zgodzić z tym stwierdzeniem przyjrzyjmy się tym nowym funkcjonalnościom.

Co nowego w Javie 9?

Moduły

Podstawową jednostką Javy są klasy, które posiadają pola wraz z metodami. Następnie klasy oraz interfejsy grupowane są w pakiety. Wraz z Javą 9 deweloperzy Oracle wprowadzili nowy sposób na organizację kodu źródłowego. Moduły, czyli zbiory pakietów, są dodatkową warstwą abstrakcji, która sprawia, że nasz projekt jest bardziej ustrukturyzowany. Dzięki czemu łatwiej jest się nam w nim poruszać oraz możemy wprowadzić dodatkową kontrolę dostępu do pakietów znajdujących się w danym module.

Definicję modułu umieszczamy w pliku module-info.java, który zawiera informacje o tym od jakich innych modułów jest on zależny oraz co wystawia jako swoje zewnętrzne API. Przyjrzyjmy się przykładowi:

1
2
3
4
5
module pl.csanecki.foo {
  requires org.apache.commons.collections;
  requires org.apache.commons.lang3;
  exports pl.csanecki.foo.api;
}

Na podstawie wyżej przedstawionego kodu widać, że nasz moduł potrzebuje dostępu do pakietów Apache Commons, natomiast swoje zewnętrzne API wystawia poprzez pakiet pl.csanecki.foo.api. Z tego powodu kodując wewnątrz modułu możemy odwoływać się tylko do zewnętrznych klas oraz interfejsów z pakietów org.apache.commons.collections oraz org.apache.commons.lang3. W ten sposób nie uzależniamy się od wewnętrznej implementacji tych bibliotek. Taka sama zasada obowiązuje w drugą stronę. Stosując klauzulę exports informujemy, że z naszym modułem można komunikować się tylko przy pomocy tego jednego, konkretnego pakietu.

JShell

Teraz kod Javy można pisać z poziomu konsoli uzyskując możliwość testowania “ad hoc” krótkich fragmentów kodu np. operacji na strumieniach. Warto wspomnieć, że nie ma konieczności pisania w konsoli klas czy interfejsów. Możemy pisać kod tak jakbyśmy byli w metodzie.

Główną zaletą takiego podejścia jest możliwość szybkiego sprawdzenia nowego API, którym jesteśmy zainteresowani. W prosty sposób można również zaprezentować komuś dane rozwiązanie bez potrzeby uruchamiania swojego ulubionego IDE. Dodatkowo sprawdza się idealnie podczas nauki podstaw Javy.

Collection Factory Methods

Wprowadzono metody fabryczne w kolekcjach, które w łatwy sposób pozwalają nam tworzyć np. listy bądź mapy. Należy dodać, że w ich przypadku zwracane są niemutowalne implementacje (łamiące zasadę Barbary Liskov).

1
2
3
4
List<Integer> list = List.of(1, 2, 3);
list.getClass(); // class java.util.ImmutableCollections$ListN
Map<Integer, String> map = Map.of(1, "A", 2, "B", 3, "C");
map.getClass(); // class java.util.ImmutableCollections$Map1

Prywatne metody w interfejsach

Wraz z wprowadzeniem Javy 8 istniała możliwość tworzenia domyślnych implementacji metod w interfejsach poprzez użycie słowa kluczowego default. Z tego powodu nowa wersja pozwala dodawać prywatne metody, których zadaniem jest uproszczenie implementacji logiki zawartej w metodach statycznych jak i domyślnych.

Nowe metody w stream API

Dodano nowe metody pozwalające manipulować danymi w strumieniach. Do użycia mamy cztery nowe metody:

  • interate - zachowuje się w podobny sposób jak pętla for. Przyjmuje trzy parametry:
    • wartość początkową licznika
    • warunek ograniczający
    • modyfikator licznika
1
2
3
4
5
6
7
DoubleStream.iterate(2.5, x -> x <= 45, x -> x * x)
  .forEach(System.out::println);

/* --- wynik --- */
// 2.5
// 6.25
// 39.0625
  • takeWhile - zwraca kolejne elementy strumienia aż do momentu, kiedy nie zostanie spełniony podany warunek:
1
2
3
4
5
6
Stream.of("J", "a", "v", "a", "9", "vs", "J", "a", "v", "a", "8")
  .takeWhile(x -> !x.equals("vs"))
  .forEach(System.out::print);

/* --- wynik --- */
// Java9
  • dropWhile - zaczyna dopiero zwracać elementy strumienia od momentu, kiedy zostanie spełniony podany warunek:
1
2
3
4
5
6
Stream.of("I", " ", "l", "o", "v", "e", " ", "J", "a", "v", "a")
  .dropWhile(x -> !x.equals("J"))
  .forEach(System.out::print);

/* --- wynik --- */
// Java
  • ofNullable - daje możliwość stworzenia strumienia z nulla (głównie pomocne przy używaniu referencji do kolekcji, które mogą być nullem):
1
2
3
4
5
Stream.ofNullable(null)
  .forEach(System.out::println);

/* --- wynik --- */
// (brak wyniku)

Dodanie nowych metod w Optional

  • stream - możliwość tworzenia strumienia z Optinal:
1
2
3
4
5
6
Optional.of("jdk9")
  .stream()
  .forEach(System.out::println);

/* --- wynik --- */
// jdk9
  • ifPresentOrElse - połączenie metody ifPresent oraz argumentu w postaci instrukcji, która wykona się, gdy brakuje wartości:
1
2
3
4
5
6
7
8
Optional.empty()
  .ifPresentOrElse(
    System.out::println, 
    () -> System.out.println("empty")
  );

/* --- wynik --- */
// empty
  • or - jeśli dany Optional jest pusty możemy go podmienić inną wartością opakowaną w Optional:
1
2
3
4
5
6
Optional.empty()
  .or(() -> Optional.of(123))
  .ifPresent(System.out::println);

/* --- wynik --- */
// 123

Co nowego w Javie 10?

var - wnioskowanie typów zmiennych lokalnych

Wraz z Javą 10 dodano nowy typ zmiennych var, który wyręcza nas z obowiązku deklarowania konkretnego typu zmiennej. Należy podkreślić fakt, że ta funkcjonalność dotyczy tylko deklaracji zmiennych lokalnych. Poniżej przedstawiam przykład działania tego mechanizmu:

1
2
3
public void method() {
  var variable = "New functionality";
}

Taki zapis automatycznie wnioskuje typ zmiennej variable jako String. Niestety ze względu na kompatybilność wsteczną nie można było uznać var jako słowa kluczowego. Wynika to z faktu, że istniała wcześniej możliwość nazywania w ten sposób zmiennych bądź części nazwy pakietów. Jest to jednak przydatny mechanizm podczas używania rozbudowanych wrażeń lambda, który zwalnia programistę z odgadywania zwracanego typu (chociaż np. Intellij przy pomocy Ctrl+Alt+V sam nam wstawi typ jaki zostanie zwrócony z danego wyrażenia).

Dodanie metod fabrykujących copyOf()

API kolekcji zostało po raz kolejny rozszerzone. Tym razem dodano wygodny sposób na kopiowanie list, map i słowników.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
List<Integer> listOfIntegers = List.of(1, 2, 3);
List<Integer> copyOfList = List.copyOf(listOfIntegers);
System.out.println(listOfIntegers.equals(copyOfList));

Set<String> setOfStrings = Set.of("A", "B", "C");
Set<String> copyOfSet = Set.copyOf(setOfStrings);
System.out.println(setOfStrings.equals(copyOfSet));

Map<Integer, String> mapIntegerToString = Map.of(1, "A", 2, "B", 3, "C");
Map<Integer, String> copyOfMap = Map.copyOf(mapIntegerToString);
System.out.println(mapIntegerToString.equals(copyOfMap));

/* --- wynik --- */
// true
// true
// true

Dodatkowo dodano metody, które pozwalają na tworzenie niemodyfikowalnych kolekcji. Są to kolejno: toUnmodifiableListtoUnmodifiableSettoUnmodifiableMap. Należy znowu podkreślić, że łamią one zasadę Liskov Substition Principle.

Co nowego w Javie 11?

Uruchomienie bez kompilacji

Zamiast kompilować ręcznie plik Javy i dopiero go uruchamiać, dostaliśmy możliwość wykonania tego w jednej linijce. Czyli poniższe instrukcje:

javac MyClass.java
java MyClass

Możemy zamienić po prostu na:

java MyClass.java

W ten sposób plik z klasą jest kompilowany “w locie”, a następnie natychmiast wykonywany.

Wprowadzenie LTS dla wybranych wersji

Główne releasy Javy, które wydawane są co 3 lata, mają mieć dłuższy okres utrzymania wynoszący 3 lata. Java 11 miała być właśnie pierwszym z nich, następnymi w kolejności są wersje 17 oraz 23. Jednak, aby otrzymać tak długi okres supportu należy wykupić subskrypcję. W przypadku braku takiego zakupu okres utrzymania staje się krótkoterminowy i wynosi tylko 6 miesięcy. Z tego powodu najlepiej jest aktualizować wersję Javy co pół roku, żeby otrzymywać wsparcie Oracle. Jasne jest jednak, że taki sposób prowadzenia projektu jest bardzo problematyczny zwłaszcza przy rozbudowanych systemach.

Użycie var w lambdach

Od 11 wersji Javy istnieje możliwość wnioskowania typów w wyrażeniach lambda poprzez użycie typu var. Dzięki czemu możemy zapisać kod w następującej postaci:

1
2
3
List.of(1, 2, 3).stream()
  .filter((var s) -> s > 1)
  .forEach(System.out::println);

Jak dla mnie jest to dosyć niezrozumiałe usprawnienie, ponieważ już istniało w Javie 8 automatyczne wnioskowanie typów w lambdach:

1
2
3
List.of(1, 2, 3).stream()
  .filter(s -> s > 1)
  .forEach(System.out::println);

Podobno dzięki tej funkcjonalności można stosować adnotacje w wyrażeniach lambda, co może zwiększyć czytelność kodu w przypadku, gdy nazwy typów są zbyt rozbudowane. Niestety nie widziałem takiego użycia, więc ciężko mi się do tego ustosunkować.

Rozszerzenie API klasy String

Klasa String doczekała się odświeżenia, dodano do niej następujące metody:

  • isBlank - weryfikuje czy na dany ciąg znaków składają się tylko białe znaki:
1
2
3
4
System.out.println("\t    \n".isBlank());

/* --- wynik --- */
// true
  • stripLeading - usuwa białe znaki z początku Stringa:
1
2
3
4
System.out.println("    Spacje po lewej".stripLeading());

/* --- wynik --- */
// Spacje po lewej
  • stripTrailing - usuwa białe znaki z końca Stringa:
1
2
3
4
System.out.println("Spacje po prawej    ".stripTrailing());

/* --- wynik --- */
// Spacje po prawej
  • strip - usuwa białe znaki z początku oraz końca Stringa:
1
2
3
4
System.out.println("    Spacje wszędzie    ".strip());

/* --- wynik --- */
// Spacje wszędzie
  • repeat - powtarza dany ciąg znaków podaną ilość razy:
1
2
3
4
System.out.println("Dlaczego?".repeat(5));

/* --- wynik --- */
// Dlaczego?Dlaczego?Dlaczego?Dlaczego?Dlaczego?
  • lines - rozbija dany String na strumień z wierszami tekstu:
1
2
3
4
5
6
7
"A\nB\nC".lines()
  .forEach(System.out::println);

/* --- wynik --- */
// A
// B
// C

Usprawnienie pracy na plikach

Dodano możliwość odczytywania i zapisywania danych do pliku bez konieczności obsługi jego otwierania - Files.readString() oraz zamykania - Files.writeString(). Nie ma też potrzeby tworzenia żadnych writerów czy readerów, wystarczy tylko wywołać odpowiednią metodę. Niby to proste, ale trzeba było czekać na takie usprawnienie bardzo długo. Dodatkowo dodana została metoda Files.isSameFile() do weryfikacji czy podane ścieżki wskazują na ten sam plik.

1
2
3
4
5
6
7
8
Files.writeString(Path.of("dane.txt"), "To jest testowy napis");
String data = Files.readString(Path.of("dane.txt"));
System.out.println(data);
System.out.println(Files.isSameFile(Path.of("dane.txt"), Path.of("dane.txt")));

/* --- wynik --- */
// To jest testowy napis
// true

Dodatki dla programowania funkcyjnego

  • not - zaprzeczenie dla danej implementacji Predicate:
1
2
3
4
5
6
Stream.of("Kotlin", "Kotlin", "Kotlin", "Kotlin", "Java", "Kotlin")
  .filter(not(s -> s.equals("Kotlin")))
  .forEach(System.out::println);

/* --- wynik --- */
// Java
  • isEmpty - sprawdzenie czy Optional nie jest pusty:
1
2
3
4
System.out.println(Optional.of("Hello World!").isEmpty());

/* --- wynik --- */
// false

Podsumowanie

Dziękuję Ci, że dotrwałeś do tego miejsca. Razem mogliśmy przejść przez nowości, które doszły wraz z wydaniem 3 nowych wersji Javy od wersji 8. W ten sposób wiadomo na jakie aspekty trzeba zwrócić uwagę podczas nauki do egzaminu z Javy 11 mając już zaplecze z ósemki. Przejście w komercyjnym projekcie na nowsze wersje Javy może być problematyczne nie ze względu samego języka programowania, ale biorąc pod uwagę biblioteki, z których korzystamy. Wadą i zaletą Javy jest to, że jest kompatybilna wstecz, więc przy migracji nie powinno być większych niespodzianek. Jednak aktualizacja bibliotek do wersji, które wspierają Javę 11, może spędzić nam sen z powiek. Z tego powodu polecam podbijanie numerka wersji bibliotek pojedynczo każdorazowo sprawdzając czy ich działanie nie wpłynęło na wynik działania aplikacji.

Czy w Twoim projekcie zdarzyła się migracja z jednej wersji Javy na nowszą? Przed jakimi wyzwaniami wtedy stanął Twój zespół? A może zdawałeś egzamin z Javy 11 mają już opanowane podstawy Javy 8? Podziel się swoją historią w komentarzu bądź napisz do mnie maila! Z wielką chęcią zapoznam się z Twoją opinią.