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
bezCast
(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.