Nowości w Java 14

Język Java, chociaż powstał w roku 1995, jest nadal rozwijany. W kolejnych wersjach dodawane są do niego nowe funkcjonalności, dzięki którym mamy coraz więcej możliwości na zapisywanie kodu w naszych programach. Przykładem są chociażby wyrażenia lambda, wprowadzone do języka w wersji 8, czy też moduł modularyzacji zależności, wprowadzony w wersji 9.

W 2018 firma Oracle, która jest właścicielem języka Java, postanowiła wydawać kolejne wersje co pół roku (więcej informacji na oficjalnym blogu). Ma to celu usystematyzowanie cyklu wydawania kolejnych wersji Java. Oracle zmienił także numerację wersji – po wersji 1.8 nastąpiła wersja 9.

W marcu 2020 wydana została najnowsza, 14 wersja Java. W tym artykule przedstawię pięć funkcjonalności, które są dostępne od tej wersji. Trzy z nich to tzw. funkcjonalności „preview”, z czego jedna była już dostępna w wersji 13. Zanim zaprezentuję wspomniane nowości, wyjaśnię pokrótce, czym są wspomniane „funkcjonalności preview”, a także gdzie znaleźć informacje o zmianach wprowadzanych do platformy Java w kolejnych wersjach.

Propozycje nowych funkcjonalności

Java preview feature to proponowana funkcjonalność Java, która może zostać wprowadzona na stałe jako „standardowa” funkcjonalność, ale może też zostać zmieniona w kolejnych wersjach platformy Java, bądź zupełnie wycofana.

Na oficjalnej stronie firmy Oracle funkcjonalność „preview” definiowana jest w następujący sposób:

A preview feature is a new feature whose design, specification, and implementation are complete, but which is not permanent, which means that the feature may exist in a different form or not at all in future JDK releases.

Źródło: Preview language and VM features

Dana funkcjonalność może przechodzić przez kilka iteracji, tak jak w przypadku Bloków Tekstu w języku Java, o których zaraz Ci opowiem. Ta funkcjonalność została wprowadzona w wersji Java 13 i miała właśnie status „preview”, natomiast w wersji Java 14 ma status „second preview”.

Funkcjonalności „preview” są opisane w tzw. „JEPach”. JEP to JDK Enhancment Proposal, czyli dokument opisujący proponowane zmiany dla platformy Java. JEPy przygotowywane są przez firmę Oracle, a służą one do opisu funkcjonalności, które mają wejść do platformy Java. Zaakceptowane JEPy wchodzą w skład zmian wprowadzanych do kolejnych wersji Java.

Dla przykładu, JEP dotyczący wprowadzenia do języka Java wyrażeń lambda to JEP 126: Lambda Expressions & Virtual Extension Methods.

JEPy mają na celu zaproponowanie zmian i umożliwienie ich przetestowania przez programistów języka Java na całym świecie. Mogą oni wyrazić swoje opinie i wpłynąć na sposób, w jaki proponowana funkcjonalność będzie istnieć w języku Java. Dzięki temu, firma Oracle może dowiedzieć się, czy dana funkcjonalność w zaproponowanej postaci jest przydatna dla programistów oraz jak, ewentualnie, ją usprawnić.

Omawiając poniżej kilka z nowych funkcjonalności języka Java 14 zamieszczę linki do oficjalnych JEPów, w których zostały zaproponowane.

Listę wszystkich JEPów znajdziesz na oficjalnej stronie.

Korzystanie z funkcjonalności Preview

Domyślnie, funkcjonalności „preview” nie są automatycznie dostępne – kompilator, widząc kod skojarzony z nową składnią bądź cechą języka Java, zaprotestuje. Aby poinformować kompilator, że chcemy korzystać z funkcjonalności „preview”, należy podczas kompilacji kodu źródłowego przekazać do kompilatora argumenty --enable-preview oraz --release. Po argumencie --release należy podać wersję Java, z której funkcjonalności „preview” mają być wzięte pod uwagę. Uruchamiając taką klasę należy ponownie podać argument --enable-preview:

javac --enable-preview --release 14 MojaKlasa.java java --enable-preview MojaKlasa

Będę korzystał z tych argumentów w jednym z kolejnych rozdziałów, gdy będę omawiał niektóre z nowych funkcjonalności wprowadzonych do języka Java w wersji 14.

Na końcu tego artykułu pokażę Ci, jak uruchamiać programy korzystające z funkcjonalności „preview” w IntelliJ IDEA.

Lista zmian kolejnych wersji Java

Jeżeli chciałbyś się dowiedzieć, jakie zmiany były wprowadzane do kolejnych wersji Java, to znajdziesz je na oficjalnej stronie firmy Oracle:

https://www.oracle.com/java/technologies/javase/jdk-relnotes-index.html

Powyższa strona zawiera linki do spisów zmian dla różnych wersji Java, od aktualnej aż do wersji 1.6. W opisach zmian znajdziesz m. in. linki do JEPów, o których mówiłem w poprzednim rozdziale.

Podsumowanie wydanych wersji Java i wprowadzonych do nich zmian znajdziesz także na Wikipedii:

https://en.wikipedia.org/wiki/Java_version_history

Pięć nowości w Java 14

Do języka Java 14 wprowadzono kilka interesujących funkcjonalności – poniżej zaprezentuję pięć z nich. Miej na uwadze, że trzy ostatnie to na razie funkcjonalności „preview”, więc, jak wspomniałem w rozdziale o JEPach, mogą one zostać zmienione w kolejnych wersjach, bądź całkowicie wycofane.

Wyrażenia switch – JEP-361

W ramach wersji 14 języka Java, wyrażenia switch stały się standardową funkcjonalnością języka Java, wcześniej mając status „preview” w wersjach 12 i 13. Używanie switch jako wyrażenia to nie jedyna zmiana dotycząca switch – od teraz możemy instrukcję skojarzoną z sekcją case wskazać używając nowej składni -> pewnaInstrukcja, bez potrzeby dodawania instrukcji break.

Przed 14 wersją języka Java mogliśmy stosować instrukcję warunkową switch, której celem było wykonanie pewnego zestawu instrukcji na podstawie dopasowania wartości danego wyrażenia do wartości wyszczególnionych w sekcjach case:

// przed Javą 14
int dzienTygodnia = 6;

String nazwaDnia;

switch (dzienTygodnia) {
  case 1:
    nazwaDnia = "Poniedziałek";
    break;
  case 2:
    nazwaDnia = "Wtorek";
    break;
  case 3:
    nazwaDnia = "Środa";
    break;
  case 4:
    nazwaDnia = "Czwartek";
    break;
  case 5:
    nazwaDnia = "Piątek";
    break;
  case 6:
    nazwaDnia = "Sobota";
    break;
  case 7:
    nazwaDnia = "Niedziela";
    break;
  default:
    nazwaDnia = "Nieprawidłowy numer dnia.";
}

System.out.println(nazwaDnia);

Celem powyższej instrukcji switch jest przypisanie wartości do zmiennej nazwaDnia na podstawie wartości zmiennej dzienTygodnia. Zwróćmy uwagę, że na końcu każdej sekcji case należało dodać instrukcję break – gdyby jej zabrakło, to po dopasowaniu zmiennej dzienTygodnia do jednej z wartości, wykonane zostałyby także instrukcje z kolejnych sekcji case.

Od wersji 14 języka Java możemy powyższy kod zapisać w krótszy i bardziej czytelny sposób. Zamiast stosować instrukcję switch, możemy użyć wyrażenia switch, wraz z nową składnią zwracania wartości:

int dzienTygodnia = 6;

String nazwaDnia = switch (dzienTygodnia) {
  case 1 -> "Poniedziałek";
  case 2 -> "Wtorek";
  case 3 -> "Środa";
  case 4 -> "Czwartek";
  case 5 -> "Piątek";
  case 6 -> "Sobota";
  case 7 -> "Niedziela";
  default -> "Nieprawidłowy numer dnia.";
};

System.out.println(nazwaDnia);

Zauważ, że wynik wyrażenia switch przypisujemy bezpośrednio do zmiennej nazwaDnia. Dodatkowo, sekcje case nie zawierają już instrukcji break, a wskazanie na zwracaną wartość w sekcjach case wykonywane jest za pomocą nowej składni -> wartość.

Składnię -> wartość możemy także stosować w instrukcjach switch. Spójrz na poniższy przykład zapisany za pomocą składni sprzed wersji Java 14:

// przed Javą 14
int dzienTygodnia = 6;

switch (dzienTygodnia) {
  case 6:
  case 7:
    System.out.println("Weekend!");
    break;
  default:
    System.out.println("Praca.");
}

W wersji Java 14, powyższy kod możemy zapisać w następujący sposób:

// Java 14
switch (dzienTygodnia) {
  case 6, 7 -> System.out.println("Weekend!");
  default -> System.out.println("Praca.");
}

Nie tylko nie musimy stosować instrukcji break, ale także zapis kilku wartości skojarzonych z pewną instrukcją został skrócony – zamiast pisać:

case 6:
case 7:
  pewna_instrukcja;

Możemy napisać:

case 6, 7 -> pewna_instrukcja;

Z nowymi wyrażeniami switch związany jest jeszcze jeden nowy mechanizm: instrukcja yield. Służy ona do zwracania wartości z bloku case wyrażenia switch (nie instrukcji switch) w jednym z dwóch przypadków:

  • gdy korzystamy ze starej składni case wartość: zamiast case wartość ->
  • gdy chcemy wykonać więcej niż jedną instrukcję korzystając ze składni case wartość ->

Pierwszy z powyższych przypadków obrazuje poniższy fragment kodu:

int dzienTygodnia = 6;

String nazwaDnia = switch (dzienTygodnia) {
  case 1: yield "Poniedziałek";
  case 2: yield "Wtorek";
  case 3: yield "Środa";
  case 4: yield "Czwartek";
  case 5: yield "Piątek";
  case 6: yield "Sobota";
  case 7: yield "Niedziela";
  default: yield "Nieprawidłowy numer dnia.";
};

System.out.println(nazwaDnia);

Zauważ, że zamiast nowej składni:

case wartość ->

używamy dotychczasowej składni:

case wartość:

W takim przypadku musimy skorzystać z instrukcji yield, aby wskazać, jaka wartość ma być zwrócona w konkretnym przypadku.

Przykład drugiego z opisanych powyżej przypadków, gdy należy stosować instrukcję yield:

int dzienTygodnia = 6;

String nazwaDnia = switch (dzienTygodnia) {
  case 1 -> "Poniedziałek";
  case 2 -> "Wtorek";
  case 3 -> "Środa";
  case 4 -> "Czwartek";
  case 5 -> "Piątek";
  case 6 -> "Sobota";
  case 7 -> "Niedziela";
  default -> {
    System.out.println("Podano nieprawidłowy numer dnia.");
    yield "?";
  }
};

System.out.println(nazwaDnia);

Ponieważ w przypadku sekcji default powyższego wyrażenia switch chcemy wykonać więcej niż jedną instrukcję, zostały one opakowane w nawiasy klamrowe, a wskazanie na wartość, jaka ma być zwrócona w tym przypadku, zostało wykonane za pomocą instrukcji yield.

Uwaga: w wyrażeniach switch nie można mieszać składni:
case wartość: yield inna_wartość;
ze składnią:
case wartość -> inna_wartość;

Ostatnia istotna informacja na temat nowych wyrażeń switch: wyrażenie switch musi zwracać wartość bądź kończyć się rzuceniem wyjątku. Nie może być sytuacji, w której pewna wartość bądź wartości nie są brane pod uwagę w wyrażeniu switch, co mogłoby spowodować, że wyrażenie jako całość nie miałoby wartości – spójrz na poniższy, nieprawidłowy przykład:

public static String nazwaDnia(int numerDnia) {
  return
    switch (numerDnia) {
      case 1 -> "Poniedziałek";
      case 2 -> "Wtorek";
      case 3 -> "Środa";
      case 4 -> "Czwartek";
      case 5 -> "Piątek";
      case 6 -> "Sobota";
      case 7 -> "Niedziela";
    };
}

W metodzie nazwaDnia korzystamy z wyrażenia switch – zauważ, że bierzemy pod uwagę jedynie siedem możliwych wartości argumentu numerDnia. Kompilator zgłosi błąd „the switch expression does not cover all possible input values” – zapomnieliśmy o sekcji default. Istnieje więc szansa, że wyrażenie switch nie zwróciłoby żadnej wartości – kompilator wykrywa i zgłasza ten błąd.

Poniższa wersja metody nazwaDnia, z dodaną sekcją default, jest poprawna:

public static String nazwaDnia(int numerDnia) {
  return
    switch (numerDnia) {
      case 1 -> "Poniedziałek";
      case 2 -> "Wtorek";
      case 3 -> "Środa";
      case 4 -> "Czwartek";
      case 5 -> "Piątek";
      case 6 -> "Sobota";
      case 7 -> "Niedziela";
      default -> "Nieprawidłowy numer dnia.";
    };
}
Więcej informacji o wyrażeniach switch znajdziesz na oficjalnej stronie: JEP-361: Switch Expressions (Standard).

Pomocne wyjątki NullPointerException – JEP-358

Wraz z nową wersją Java otrzymaliśmy usprawnienie dotyczące wyjątku NullPointerException.

Komunikaty towarzyszące temu wyjątkowi zostały rozszerzone o przydatną informację, dzięki której wiemy dokładnie, który obiekt jest powodem rzucenia wyjątku. Ma to znaczenie w przypadku odwoływania się do zagnieżdżonych pól, wywoływania metod na zagnieżdżonych obiektach, bądź odwoływania się do kolejnych elementów wielowymiarowych tablic, gdy któryś z obiektów „po drodze” ma wartość null.

Spójrzmy na kilka przykładów programów, które zaprezentują nową funkcjonalność w akcji.

Uruchomienie poniższego fragmentu kodu skutkuje wyjątkiem NullPointerException, ponieważ nie przypisaliśmy żadnego obiektu do pola b obiektu a:

class A {
  public B b;
}

class B {
  public String s;
}

public class PomocneNPE {
  public static void main(String[] args) {
    A a = new A();

    // błąd – obiekt b jest nullem
    System.out.println(a.b.s);
  }
}

Jeżeli powyższy kod wykonamy na wersji Java 13 lub wcześniejszej, to zobaczymy następujący komunikat:

$ javac PomocneNPE.java $ java PomocneNPE Exception in thread "main" java.lang.NullPointerException at PomocneNPE.main(PomocneNPE.java:13)

Maszyna Wirtualna Java poinformowała nas o typie wyjątku, jaki został rzucony, a także podała nazwę klasy i linię, która spowodowała zaistnienie wyjątku.

Spójrzmy teraz jak wygląda komunikat powyższego wyjątku NullPointerException, gdy uruchomimy kod na wersji Java 14:

$ java -XX:+ShowCodeDetailsInExceptionMessages PomocneNPE Exception in thread "main" java.lang.NullPointerException: Cannot read field "s" because "<local1>.b" is null at PomocneNPE.main(PomocneNPE.java:13)

Tym razem komunikat się różni – zwróć uwagę na dodatkową informację:

Cannot read field "s" because "<local1>.b" is null

Maszyna Wirtualna Java wskazała nam, który z obiektów jest nullem – jest to <local1>.b, czyli pole b obiektu a tworzonego w metodzie main klasy PomocneNPE. Dodatkowo widzimy opis akcji, który doprowadził do wyjątku – Cannot read field "s" – w tym przypadku była to próba odniesienia się do pola obiektu.

Dlaczego widzimy generyczną nazwę <local1> zamiast po prostu nazwy „a” zmiennej lokalnej oraz dlaczego uruchamiając klasę PomocneNPE musieliśmy przekazać do Maszyny Wirtualnej Java argument -XX:+ShowCodeDetailsInExceptionMessages?

Maszyna Wirtualna Java nie zna nazw zmiennych lokalnych, dlatego w komunikacie „pomocnego” NPE umieszcza generyczną nazwę <local1>. Gdybyśmy skompilowali klasę PomocneNPE z argumentem -g (debug info), to w klasie zostałyby zawarte wszystkie dodatkowe informacje związane z kompilowanym kodem. Uruchomienie takiej klasy w Maszynie Wirtualnej Java spowodowałoby, że komunikat „pomocnego” NPE zawierałby nazwę zmiennej lokalnej:

$ javac -g PomocneNPE.java $ java -XX:+ShowCodeDetailsInExceptionMessages PomocneNPE Exception in thread "main" java.lang.NullPointerException: Cannot read field "s" because "a.b" is null at PomocneNPE.main(PomocneNPE.java:13)

Tym razem, komunikat wyjątku zawiera nazwę zmiennej lokalnej a.

Pozostaje jeszcze kwestia argumentu -XX:+ShowCodeDetailsInExceptionMessages.

Funkcjonalność „pomocnego” NPE nie jest domyślnie włączana za każdym razem, gdy uruchamiamy Maszynę Wirtualną Java – jeżeli chcemy, aby wyjątki NPE były „pomocne”, musi dodać wspomniany argument. „Pomocne” NPE mają być domyślnie włączone w jednej z kolejnych wersji języka Java, o czym możemy się dowiedzieć w oficjalnym JEPie dotyczącym tej funkcjonalności:

It is intended to enable code details in exception messages by default in a later release.

Źródło: JEP-358: Helpful NullPointerExceptions

Spójrzmy na jeszcze dwa przykłady komunikatów „pomocnych” NPE. W poniższym programie próbujemy wywołać metodę na obiekcie, który jest nullem:

class X {
  String s;
}

public class PomocneNPE2 {
  public static void main(String[] args) {
    X x = new X();

    System.out.println(x.s.length());
  }
}

Przed wersją Java 14, zobaczylibyśmy następujący komunikat po uruchomieniu tego programu:

Exception in thread "main" java.lang.NullPointerException at PomocneNPE2.main(PomocneNPE2.java:9)

Komunikat dla wersja Java 14 jest z kolei następujący:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "x.s" is null at PomocneNPE2.main(PomocneNPE2.java:9)

Treść wyjątku zawiera informację, że pole s obiektu x jest nullem: because "x.s" is null. Dodatkowo opisana została akcja, która do tego wyjątku doprowadziła – próbowaliśmy wywołać metodę: Cannot invoke "String.length()".

Ostatnim przykładem, na który spojrzymy, jest próba odniesienia się do nullowego elementu tablicy:

public class PomocneNPE3 {
  public static void main(String[] args) {
    int[][] tab = new int[2][];

    System.out.println(tab[0][1]);
  }
}

Przed wersją Java 14, komunikat wyglądał następująco:

Exception in thread "main" java.lang.NullPointerException at PomocneNPE3.main(PomocneNPE3.java:5)

Natomiast korzystając z wersji Java 14, na ekranie zobaczymy:

Exception in thread "main" java.lang.NullPointerException: Cannot load from int array because "tab[0]" is null at PomocneNPE3.main(PomocneNPE3.java:5)

Ponownie widzimy rozszerzony komunikat, w którym Maszyna Wirtualna Java informuje nas, który obiekt zawinił ("tab[0]" is null), a także jaką akcję próbowaliśmy wykonać w instrukcji, która doprowadziła do wystąpienia wyjątku (Cannot load from int array).

Więcej informacji o zmianach związanych z wyjątkiem NullPointerException znajdziesz na oficjalnej stronie: JEP-358: Helpful NullPointerExceptions.

Rekordy (preview) – JEP-359

W ramach JEP-359, Java otrzymała nowy typ danych: rekordy. Ich celem jest uproszczenie tworzenia typów, których jedynym celem jest przechowywanie pewnego zestawu danych. Rekordy pozwalają na zwięzłe definiowanie struktur danych, które z założenia mają być niemutowalne, gdyż pola rekordów są z finalne (final).

Ta funkcjonalność ma obecnie status preview, więc może się zmienić w przyszłych wersjach języka Java, bądź zostać całkowicie wycofana. Aby programy, które z niej korzystają, kompilowały się bez błędów, należy przekazać kompilatorowi javac argumenty --enable-preview --release 14, a Maszynie Wirtualnej Java argument --enable-preview:

javac --enable-preview --release 14 NazwaKlasy.java java --enable-preview NazwaKlasy

Rekordy posiadają nazwę oraz definicję danych, jakie mają móc reprezentować. Definiujemy je za pomocą nowego słowa kluczowego record. Spójrzmy na przykład:

record Punkt(int x, int y) {}

Zdefiniowany powyżej typ to rekord o nazwie Punkt. W nawiasach, po nazwie typu, znajduje się definicja pól, jakie obiekty tego typu będą przechowywać. Obiekty tego nowego typu tworzymy tak samo, jak obiekty zwykłych klas – za pomocą słowa kluczowego new.

Zawauż, że ciało rekordu jest puste. Domyślnie, każdy rekord posiada następujące, automatycznie wygenerowane przez kompilator pola i metody:

  • prywatne, finalne pola dla każdego z elementów wyszczególnionych w nawiasach po nazwie typu rekordu,
  • publiczny konstruktor, który przyjmuje takie same argumenty, jak pola umieszczone w definicji rekordu – konstruktor inicjalizuje te pola rekordu swoimi argumentami (1),
  • publiczne gettery dla wygenerowanych pól, które mają taką samą nazwę, jak pola, które zwracają (2),
  • metodę hashCode (3) oraz equals (5) (6) – dostarczana metoda equals zwraca true, jeżeli dwa obiekty typu rekordowego są tego samego typu oraz ich pola są sobie równe,
  • implementację metody toString, która zwraca nazwę typu obiektu oraz nazwy i wartości jego pól (4).

Spójrz na przykład użycia elementów typu rekordowego, która są dla nas automatycznie generowane:

record Punkt(int x, int y) {}

public class Rekordy {
  public static void main(String[] args) {
    Punkt p1 = new Punkt(10, 20); // 1

    System.out.println(p1.x() + ", " + p1.y()); // 2
    System.out.println("Hash code: " + p1.hashCode()); // 3
    System.out.println(p1); // 4

    Punkt p2 = new Punkt(6, 4);
    Punkt p3 = new Punkt(10, 20);

    System.out.println(p1.equals(p2)); // 5
    System.out.println(p1.equals(p3)); // 6
  }
}

Wynik działania tego program:

10, 20 Hash code: 330 Punkt[x=10, y=20] false true

Rekordy nie mogą rozszerzać innych klas i rekordów, ale mogą implementować interfejsy:

record Punkt(int x, int y) implements Serializable {}

Ciała rekordów nie mogą zawierać definicji innych pól niż te, które zostały zdefiniowane w nawiasach po nazwie typu rekordowego, ale mogą zawierać pola statyczne. Ponadto, możemy w rekordach definiować także metody statyczne i zwykłe:

record Punkt(int x, int y) implements Serializable {
  public Punkt przesun(int dx, int dy) {
    return new Punkt(this.x + dx, this.y + dy);
  }
}
Więcej informacji o zmianach związanych z wprowadzeniem do języka Java rekordów znajdziesz na oficjalnej stronie: JEP-359: Records (Preview).

Usprawnienie operatora instanceof (preview) – JEP-305

W wersji Java 14, operator instanceof otrzymał nową funkcjonalność, która związana jest z mechanizmem pattern matching, który twórcy języka chcą powoli wprowadzać do różnych elementów języka Java.

Ta funkcjonalność ma obecnie status preview, więc może się zmienić w przyszłych wersjach języka Java, bądź zostać całkowicie wycofana. Aby programy, które z niej korzystają, kompilowały się bez błędów, należy przekazać kompilatorowi javac argumenty --enable-preview --release 14, a Maszynie Wirtualnej Java argument --enable-preview:

javac --enable-preview --release 14 NazwaKlasy.java java --enable-preview NazwaKlasy

W uproszczeniu, pattern matching polega na wyodrębnieniu pewnej wartości na podstawie prawdziwości danego warunku. Najłatwiej zrozumieć działanie tego mechanizmu na przykładzie nowej funkcjonalności operatora instanceof. Spójrzmy najpierw na przykład programu, który korzysta z dotychczasowej składni operatora instanceof:

public class PatternMatchingOfInstanceof {
  public static void main(String[] args) {
    Object obj = "abc";

    if (obj instanceof String) {
      String s = (String) obj; // 1
      System.out.println(s.length());
    } else {
      System.out.println("obj jest innego typu niż String.");
    }
  }
}

Jeżeli warunek obj instanceof String jest prawdziwy, to w linii (1) do lokalnej zmiennej s przypisujemy obiekt obj zrzutowany na typ String. Tak zapisywany był kod tego rodzaju przed wersją Java 14.

W wersji Java 14 powyższy kod moglibyśmy zapisać w następujący sposób:

public class PatternMatchingOfInstanceof {
  public static void main(String[] args) {
    Object obj = "abc";

    if (obj instanceof String s) { // 2
      System.out.println(s.length()); // 3
    } else {
      System.out.println("obj jest innego typu niż String.");
    }
  }
}

Zauważ, że zmienił się warunek instrukcji if – po typie String znajduje się nazwa zmiennej s (2). Ponadto, ciało instrukcji nie zawiera już rzutowania zmiennej obj na String. Pozostała jedynie instrukcji wypisująca na ekran liczbę znaków (3). Powyższy przykład obrazuje pattern matching operatora instanceof w akcji.

Dzięki tej nowej funkcjonalności, nie musimy ręcznie rzutować obiektu, którego typ sprawdzamy – jeżeli warunek pewnaZmienna instanceof PewienTyp będzie spełniony, to pewnaZmienna zostanie automatycznie zrzutowana na typ PewienTyp i umieszczona w zmiennej, której nazwę podaliśmy po nazwie typu.

Ze zmiennej, do której zostanie dla nas przypisana zrzutowana wartość, możemy korzystać w kolejnych warunkach instrukcji if:

if (obj instanceof String s && s.length() > 5) {
  System.out.println(s.length());
}

Opisane powyżej zmiany wprowadzone do operatora instanceof mają na celu zmniejszenie liczby „ręcznego” rzutowania, które muszą być wykonywane przez programistów.

Więcej informacji o zmianach związanych z pattern matching operatora instanceof znajdziesz na oficjalnej stronie: JEP-305: Pattern Matching of instanceof (Preview).

Bloki tekstu (second preview) – JEP-368

Od wersji Java 13 otrzymaliśmy możliwość definiowania wielolinijkowych stringów bezpośrednio w kodzie Java, bez wymogu korzystania z operatora + do konkatenacji kolejnych linii, które miały stanowić blok tekstu. Po zebraniu opinii programistów, Oracle postanowił udoskonalić ten mechanizm. Kolejna wersja tej funkcjonalności trafiła do wersji Java 14 i ma status second preview.

Ta funkcjonalność ma obecnie status preview, więc może się zmienić w przyszłych wersjach języka Java, bądź zostać całkowicie wycofana. Aby programy, które z niej korzystają, kompilowały się bez błędów, należy przekazać kompilatorowi javac argumenty --enable-preview --release 14, a Maszynie Wirtualnej Java argument --enable-preview:

javac --enable-preview --release 14 NazwaKlasy.java java --enable-preview NazwaKlasy

Blok tekstu zaczyna się i kończy trzema cudzysłowami:

String str = """
    Witaj na Kurs Java!
    kursjava.com
    """;

System.out.println(str);

Wartość zmiennej str wypisana na ekran to:

Witaj na Kurs Java! kursjava.com

Warto tutaj zauważyć trzy właściwości bloków tekstu:

  • po każdej linii tekstu dodawany jest znak nowej linii,
  • początkowe znaki spacji są wycinane,
  • string, który zostanie przypisany do zmiennej str, zaczyna się od pierwszej linii następującej po znakach """, które otwierają blok tekstu.

Wartość przypisana do zmiennej str, zapisana bez nowej funkcjonalności, wyglądałaby następująco:

String strConcat =
    "Witaj na Kurs Java!\n" +
    "kursjava.com\n";

Definiując blok tekstu, po otwierających go cudzysłowach musimy przejść do nowej linii – w przeciwnym razie kod się nie skompiluje:

str = """x"""; // błąd

Błąd kompilacji jest następujący:

TextBlocks.java:13: error: illegal text block open delimiter sequence, missing line terminator str = """x"""; // błąd ^ TextBlocks.java:13: error: illegal text block open delimiter sequence, missing line terminator str = """x"""; // błąd ^ Note: TextBlocks.java uses preview language features. Note: Recompile with -Xlint:preview for details. 2 errors

Jak już wspomniałem powyżej, początkowe białe znaki są wycinane z bloku tekstu. Tym zachowaniem można sterować za pomocą odpowiedniego umiejscowienia znaków """ kończących blok tekstu – spójrzmy na poniższy przykład:

str = """
    "Go then, there are other worlds than these."
    Jake Chambers from The Dark Tower by Stephen King
    """;

System.out.println(str);

str = """
    "Go then, there are other worlds than these."
    Jake Chambers from The Dark Tower by Stephen King
""";

System.out.println(str);

Zauważ, że drugi z powyższych bloków tekstu ma umieszczone zamykające znaki """ wcześniej, niż zaczyna się string opisywany przez blok tekstu. Ten fragment kodu spowoduje wypisanie na ekran:

"Go then, there are other worlds than these." Jake Chambers from The Dark Tower by Stephen King "Go then, there are other worlds than these." Jake Chambers from The Dark Tower by Stephen King

Przesunięcie kończących znaków """ powoduje, że pewna liczba początkowych białych znaków nie jest ignorowana. W tym przypadku wszystkie cztery białe znaki weszły w skład stringa przypisywanego do zmiennej str.

Ten przykład obrazuje także, że możemy swobodnie korzystać ze znaków cudzysłowu w blokach tekstu.

Bloki tekstu mogą także opisywać długi tekst, który nie powinien być podzielone na osobne linie. Aby zaznaczyć, że znak nowej linii nie powinien być wstawiony na końcu aktualnej linii w bloku tekstu, stosujemy znak \ (backslash):

str = """
    pierwsza linia \
    nadal pierwsza linia \
    i znowu pierwsza linia
    """;

System.out.println(str);

Do zmiennej str przypisana zostanie wartość „pierwsza linia nadal pierwsza linia i znowu pierwsza linia”, bez znaków nowej linii, co widać na ekranie:

pierwsza linia nadal pierwsza linia i znowu pierwsza linia

Bloki tekstu wydają się przydatną funkcjonalnością i mam nadzieję, że wejdą na stałe do języka Java w jednej z kolejnych wersji. Wygląda na to, że tak właśnie się stanie – zgodnie z JEP-378, bloki tekstu mają wejść jako „standardowa” funkcjonalność języka Java w nadchodzącej, 15 wersji.

Więcej informacji o zmianach związanych z blokami tekstu znajdziesz na oficjalnej stronie: JEP-368: Text Blocks (Second Preview).

IntelliJ IDEA i funkcjonalności „preview”

IntelliJ IDEA domyślnie nie umożliwia korzystania z funkcjonalności „preview” w kodzie Java, jednak można to łatwo zmienić w ustawieniach projektu. Wystarczy z menu File wybrać Project Structure... lub skorzystać ze skrótu Ctrl + Alt + Shift + S. W oknie, które się pojawi, na zakładce Project, należy wybrać „14 (Preview) – Record, patterns, text blocks” z pola znajdującego się pod etykietą Project language level i kliknąć OK:

Wskazanie IntelliJ IDEA, że chcemy korzystać w naszym projekcie z funkcjonalności Java oznaczonych jako „preview”
Wskazanie IntelliJ IDEA, że chcemy korzystać w naszym projekcie z funkcjonalności Java oznaczonych jako „preview”

Od tej pory będziemy mogli w naszym projekcie korzystać z funkcjonalności „preview”.

W jednym z poprzednich rozdziałów, w którym prezentowałem zmiany związane z komunikatami wyjątków NullPointerException („pomocne” NPE), należało do Maszyny Wirtualnej Java przekazać argument -XX:+ShowCodeDetailsInExceptionMessages podczas uruchamiania programu, aby wyjątki NPE faktycznie były „pomocne”. Ten argument można ustawić w konfiguracji uruchomienia projektu:

Pole w konfiguracji uruchomienia projektu w IntelliJ IDEA, w którym można ustawić argumenty dla Maszyny Wirtualnej Java
Pole w konfiguracji uruchomienia projektu w IntelliJ IDEA, w którym można ustawić argumenty dla Maszyny Wirtualnej Java

Więcej informacji o konfiguracjach uruchomienia znajdziesz w moim kursie IntelliJ IDEA w akcji – konfiguracja uruchomienia projektu.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.