Nasza strona używa cookies. Korzystając ze strony, wyrażasz zgodę na używanie cookies, zgodnie z aktualnymi ustawieniami przeglądarki. Rozumiem

Najważniejsze funkcje od Javy 8 do 15 w pigułce

Philipp Gysel Software Engineer / Intersys AG
Udaj się w podróż po najważniejszych funkcjach Javy, począwszy od wersji 8, a skończywszy na wersji 15.
Najważniejsze funkcje od Javy 8 do 15 w pigułce

Opiszę tutaj wszystkie funkcje Javy, które pojawiły się od czasu „siódemki”. Skupimy się na przynajmniej jednej istotnej zmianie na wersję, począwszy od Javy 8, a skończywszy na Javie 15, którą opublikowano jesienią 2020. Java posiada teraz pełną obsługę lambd, programowanie funkcyjne, wnioskowanie typów za pomocą var, kolekcje niemutowalne z prostymi konstruktorami oraz wielolinijkowe ciągi znaków. 

Co więcej, pojawiły się również nowe, eksperymentalne funkcje, takie jak data classes (record) oraz klasy zapieczętowane. Będę również mówił o REPL Javy, który jest bardzo pomocny w szybkim eksperymentowaniu. 

Spis treści:

  • Programowanie funkcyjne (Java 8)
  • Streams (Java 8)
  • Optional (Java 8)
  • JShell (Java 9)
  • Metody wytwórcze dla niemutowalnych kolekcji (Java 9)
  • Wnioskowanie typów przy pomocy var (Java 10)
  • Single Source File Launch (Java 11)
  • Wyrażenie Switch (Java 12 - funkcja eksperymentalna, Java 14 - pełna implementacja)
  • Wielolinijkowe ciągi znaków (Java 13 - funkcja eksperymentalna, Java 14 - pełna implementacja)
  • Klasy danych: record (Java 14 - funkcja eksperymentalna)
  • instanceof bez Cast (Java 14 - funkcja eksperymentalna)
  • Klasy zapieczętowane (Java 15 - funkcja eksperymentalna)
  • Bonus: nowe warunki licencyjne, począwszy od Javy 8
  • Podsumowanie


Programowanie funkcyjne (Java 8)

W Javie 8 dodano programowanie funkcyjne oraz lambdy. Dwie główne zasady tego typu programowania to niemutowalne wartości i podniesienie rangi funkcji do „obywateli pierwszej kategorii”. 

Dane przechodzą przez szereg modyfikacji, gdzie każdy etap przyjmuje jakieś wejście i przetwarza je na dane wyjścia. Programowanie funkcyjne można wykorzystać ze Streams oraz z monadami null-safe (Optional). 


Streams (Java 8)

Jeśli tworzymy najzwyklejszy program komputerowy, to często pracujemy z listami wartości i wykonujemy na każdej wartości transformację. 

A przed Javą 8 trzeba było do tego używać pętli for. Od „ósemki” można jednak korzystać ze Streams w następujący sposób: 

Stream.of("hello", "great")
    .map(s -> s + " world")
    .forEach(System.out::println);
> hello world
> great world


Funkcja map bierze jako wejście lambdę. Lambda zostanie potem użyta we wszystkich elementach w strumieniu. Streams może działać na listach, zbiorach, mapach (przez transformację). Dzięki tej funkcji pozbędziesz się praktycznie wszystkich pętli w kodzie.


Optional (Java 8)

NullPointerException było powszechnym problemem w Javie. Wprowadzono więc Optional, czyli monadę, która opakowuje referencję, będącą lub niebędącą nullem. Aktualizację do Optional można wprowadzać za pomocą programowania funkcyjnego: 

Optional.of(new Random().nextInt(10))
    .filter(i -> i % 2 == 0)
    .map(i -> "number is even: " + i)
    .ifPresent(System.out::println);
> number is even: 6


W powyższym fragmencie kodu tworzymy losową liczbę, opakowujemy ją wewnątrz Optional i drukujemy liczbę, jeśli jest ona parzysta. 


JShell (Java 9)

Tutaj wprowadzono REPL dla Javy - jego nazwa to JShell. W skrócie JShell pozwala eksperymentować z fragmentami kodu bez pisania i kompilowania całej klasy. 

Zamiast tego można wykonywać komendę po komendzie i od razu zobaczyć rezultaty. Oto prosty przykład: 

$ <JDK>/bin/jshell
jshell> System.out.println("hello world")
hello world


Ci, którzy pisali kiedyś w interpretowanych językach, takich jak JS, czy Python, mogli już wcześniej pracować z REPL. JShell pozwala na definiowanie zmiennych oraz innych, bardziej złożonych obiektów, takich jak wielolinijkowe funkcje, klasy i pętle. Co więcej, JShell obsługuje autouzupełnianie, które przydaje się, jeśli nie wiesz, jakich dokładnie metod z danej klasy możesz użyć. 


Metody wytwórcze dla niemutowalnych kolekcji (Java 9)

W Javie długo brakowało prostej inicjalizacji Lists. Te czasy już jednak minęły. Wcześniej trzeba było robić coś takiego:

jshell> List<Integer> list = Arrays.asList(1, 2, 3, 4)
list ==> [1, 2, 3, 4]


Teraz można zrobić to w prostszy sposób:

jshell> List<Integer> list = List.of(1, 2, 3, 4)
b ==> [1, 2, 3, 4]


Metody of(...) można używać z List, Set i Map - każda z nich tworzy niemutowalny obiekt za pomocą jednej linijki kodu. 


Wnioskowanie typów z var (Java 10)

W Javie 10 wprowadzono słowo kluczowe var - pozwala ono pominąć typ zmiennej. 

jshell> var x = new HashSet<String>()
x ==> []

jshell> x.add("apple")
$1 ==> true


W powyższym fragmencie kodu kompilator mówi nam, że typ x to HashSet. Funkcja ta pomaga w zredukowaniu boilerplate’u i zwiększa czytelność kodu. Ma ona jednak kilka ograniczeń: var można używać tylko w ciele metody, a kompilator wywnioskuje typ podczas kompilacji. Wszystko nadal będzie więc statycznie typowane. 


Single Source File Launch (Java 11)

Gdy wcześniej pisaliśmy w Javie program składający się z jednego pliku, musieliśmy najpierw skompilować plik za pomocą javac, a następnie uruchomić go przy pomocy java. W Javie 11 możesz wykonać oba te kroki za pomocą jednego polecenia. Najpierw definiujemy pojedynczy plik źródłowy, czyli Main.java:

public class Main {
  public static void main(String[] args) {
    System.out.println("hello world");
  }
}


A teraz możesz go skompilować i uruchomić za jednym zamachem: 

$ java ./Main.java
hello world


W przypadku prostych programów lub eksperymentów składających się tylko z jednej klasy Java, korzystanie z tej funkcji do uruchamiania pojedynczych plików źródłowych ułatwi Ci życie.


Wyrażenie Switch (Java 12)

W Javie 12 wprowadzono wyrażenie Switch. Oto jak różni się ono od swojej poprzedniej wersji.

Stare wyrażenie switch definiuje przepływ programu:

jshell> var i = 3
jshell> String s;
jshell> switch(i) {
 ...> case 1: s = "one"; break;
 ...> case 2: s = "two"; break;
 ...> case 3: s = "three"; break;
 ...> default: s = "unknown number";
 ...> }
jshell> s
s ==> "three"


Nowe wyrażenie zwraca natomiast wartość:

jshell> var i = 3;
jshell> var x = switch(i) {
...> case 1 -> "one";
...> case 2 -> "two";
...> case 3 -> "three";
...> default -> "unknown number";
...> };
x ==> "three"


Podsumowując, stare wyrażenie switch odnosi się do kontroli przepływu programu, a nowe do wartości. Zauważcie, że nowe wyrażenie to trochę funkcja mapująca - mamy jeden input (w powyższym kodzie jest to i) i jeden output (czyli x).

W rzeczywistości jest to funkcja dopasowywania wzorców, która pomaga zwiększyć zgodność Javy z zasadami programowania funkcyjnego. Podobne wyrażenie Switch jest już od jakiegoś czasu dostępne w Scali.

Kilka rzeczy, o których warto pamiętać:

  • Zamiast dwukropka, korzystamy z ->
  • Nie trzeba używać break
  • Przypadek domyślny można pominąć, gdy rozważane są wszystkie możliwe przypadki
  • Aby włączyć tę funkcję w Javie 12, użyj --enable-preview --source 12


Wielolinijkowe ciągi znaków (Java 13)

Czy musieliście kiedyś zdefiniować długi, wielolinijkowy ciąg znaków, taki jak JSON lub XML? Do czasu tej wersji przenosiliście pewnie wszystko do jednej linijki i używaliście znaków newline \n. Utrudnia to jednak czytanie danego stringu.

Na szczęście w Javie 13 wprowadzono wielolinijkowe ciągi znaków.

Oto przykład:

public class Main { 
  public static void main(String [] args) {
    var s = """
        {
            "recipe": "watermelon smoothie",
            "duration": "10 mins",
            "items": ["watermelon", "lemon", "parsley"]
        }""";
    System.out.println(s);
  }
}


A teraz uruchomimy główną metodę za pomocą single-file-launch:

java --enable-preview --source 13 Main.java

{
"recipe": "watermelon smoothie",
"duration": "10 mins",
"items": ["watermelon", "lemon", "parsley"]
}


Wynikowy ciąg znaków obejmuje wiele linii, a cudzysłowy "" i tabulatory \t pozostają nienaruszone.


Klasy danych: record (Java 14)

To chyba najbardziej ekscytująca funkcja - wreszcie mamy w Javie klasy danych!

Klasy te są deklarowane za pomocą słowa kluczowego record i mają automatyczne gettery, konstruktor, metodę equals() itp. Krótko mówiąc, możesz się dzięki nim pozbyć ogromnego fragmentu boilerplate'u.

jshell> record Employee (String name, int age, String department) {}
| created record Employee

jshell> var x = new Employee("Anne", 25, "Legal");
x ==> Employee[name=Anne, age=25, department=Legal]

jshell> x.name()
$2 ==> "Anne"


Scala ma podobną funkcję z case classes, a Kotlin z klasami danych. Wielu programistów korzystało do tej pory w Javie z Lombok, który oferował prawie wszystkie funkcje, będące z kolei inspiracją dla rekordów w Javie 14. Więcej szczegółów można znaleźć tutaj.


instanceof bez Cast (Java 14)

Słowo kluczowe instanceof było już dostępne we wcześniejszych wersjach Javy:

Object obj = new String("hello");
if (obj instanceof String) {
  System.out.println("String length: " + ((String)obj).length());
}


Niefortunna część: najpierw sprawdzamy, czy s jest typu String, a następnie rzucamy go ponownie, by poznać jego długość. Dzięki Javie 14 kompilator jest wystarczająco inteligentny, aby wywnioskować typ automatycznie po instanceof:

Object obj = new String("hello");
if (obj instanceof String mystr) {
  System.out.println("String length: " + mystr.length());
}


Klasy zapieczętowane (Java 15)

Za pomocą słowa kluczowego sealed możesz określić, które klasy rozbudowują daną klasę lub interfejs. Oto przykład:

public sealed interface Fruit permits Apple, Pear {
    String getName();
}

public final class Apple implements Fruit {
    public String getName() { return "Apple"; }
}

public final class Pear implements Fruit {
    public String getName() { return "Pear"; }
}


W jaki sposób nam to pomaga? No cóż - teraz wiesz, ile razy występuje obiekt Fruits. W rzeczywistości jest to ważny krok w kierunku w pełni obsługiwanego dopasowywania wzorców, w którym można traktować daną klasę podobnie jak enum.

Funkcja ta ładnie zgrywa się z nowym wyrażeniem switch, o którym mówiliśmy wcześniej.


Bonus: warunki licencyjne od Javy 8

Ostatni temat, jaki tutaj poruszymy, to kwestia licencji. Większość z Was pewnie słyszała o tym, że Oracle zaprzestał aktualizacji dla darmowej wersji Java 8. Oto opcje, jakie macie:

  • Użycie nowszej wersji Oracle JDK (Oracle oferuje darmowe aktualizacje dotyczące bezpieczeństwa przez pół roku po każdym wydaniu)
  • Ryzyko i użycie starszej wersji JDK
  • Użycie starszej wersji OpenJDK - te są nadal aktualizowane przez społeczność open-source
  • Płatne wsparcie od Oracle (np. Java 8 dostaje update’y do 2030)


Poniżej widać czas trwania tymczasowego wsparcia dla każdego JDK:


Timeline wsparcia dla każdego JDK


Na nowy model licencyjny Oracle'a wpływa cykl wydań: Oracle będzie udostępniać nową wersję Javy co 6 miesięcy.

Nowy cykl wydań pomaga firmie Oracle w sprawniejszym ulepszaniu Javy, szybszym uzyskiwaniu feedbacku dzięki funkcjom eksperymentalnym oraz dogonieniu nowocześniejszych języków, takich jak Scala, Kotlin, czy Python.

Jeśli chcecie dowiedzieć się więcej, sprawdźcie ten artykuł.


Podsumowanie

Przez ostatnie 6 lat Java przebyła długą drogę - w tym czasie pojawiło się aż 8 nowych wersji tego języka!

Każda z wyżej wymienionych funkcji sprawia, że Java cały czas może konkurować z nowszymi technologiami opartymi na JVM (Scala i Kotlin). 

Dziękuję za uwagę! A jaka jest Wasza ulubiona funkcja Javy? Dajcie znać w komentarzu!


Oryginał tekstu w języku angielskim możesz przeczytać tutaj.

Rozpocznij dyskusję

Lubisz dzielić się wiedzą i chcesz zostać autorem?

Podziel się wiedzą z 160 tysiącami naszych czytelników

Dowiedz się więcej