Java 8 — przewodnik po nowoczesnej Javie

> Dodaj do ulubionych

„Java jeszcze nie umarła — i ludzie zaczynają sobie zdawać z tego sprawę”.

Witajcie w moim wprowadzeniu do języka programowania Java 8. W niniejszym przewodniku krok po kroku omawiam wszystkie nowe składniki języka. Dzięki krótkim, nieskomplikowanym przykładom kodu nauczycie się korzystać z domyślnych metod interfejsów, wyrażeń lambda, referencji do metod i powtarzalnych adnotacji. Poznacie najnowsze zmiany w interfejsach API dotyczące m.in. strumieni, interfejsów funkcyjnych, rozszerzeń słowników oraz nowy API daty i czasu. Zero przegadanych wyjaśnień — tylko fragmenty kodu opatrzone komentarzami. Miłej lektury!

Ten artykuł został po raz pierwszy opublikowany na moim blogu. Zachęcam, byście śledzili mnie na Twitterze.

Java 8 — metody domyślne w interfejsach

Java 8 umożliwia dodawanie do interfejsów implementacji metod nieabstrakcyjnych przy pomocy słowa kluczowego default. Ten składnik funkcjonalności nosi również nazwę wirtualnych metod rozszerzających.

Oto nasz pierwszy przykład:

interface Formula {
    double calculate(int a);

    default double sqrt(int a) {
        return Math.sqrt(a);
    }
}

Interfejs Formula zawiera definicję nie tylko abstrakcyjnej metody calculate, lecz także domyślnej metody sqrt. Klasy konkretne muszą implementować jedynie abstrakcyjną metodę calculate, natomiast domyślna metoda sqrt jest dostępna od razu.

Formula formula = new Formula() {
    @Override
    public double calculate(int a) {
        return sqrt(a * 100);
    }
};

formula.calculate(100);     // 100.0
formula.sqrt(16);           // 4.0

Powyżej widzimy implementację obiektu anonimowego. Kod jest jednak nieco rozwlekły: proste działanie matematyczne sqrt(a * 100) zajmuje aż 6 wierszy. Jak się przekonamy w kolejnej sekcji artykułu, w Javie 8 pojedyncze obiekty metod można zaimplementować w o wiele lepszy sposób.

Wyrażenia lambda

Zacznijmy od prostego przykładu ilustrującego sortowanie listy łańcuchów we wcześniejszych wersjach Javy:

List<String> names = Arrays.asList("peter", "anna", "mike", "xenia");

Collections.sort(names, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return b.compareTo(a);
    }
});

Statyczna metoda pomocnicza Collections.sort przyjmuje listę i komparator, dzięki któremu ustala kolejność elementów przekazanej listy. Często tworzymy więc anonimowe komparatory, które następnie przekazujemy metodzie sortującej.

Natomiast w ósmej odsłonie Javy nie tworzy się już w kółko obiektów anonimowych — dostępna jest znacznie krótsza składnia wyrażeń lamba:

Collections.sort(names, (String a, String b) -> {
    return b.compareTo(a);
});

Jak widać, kod jest o wiele krótszy i czytelniejszy. A da się go skrócić jeszcze bardziej:

Collections.sort(names, (String a, String b) -> b.compareTo(a));

W przypadku metod, których treść główna zawiera się w jednym wierszu można zrezygnować z klamr i słowa kluczowego return. To jednak nie koniec skracania:

names.sort((a, b) -> b.compareTo(a));

Lista zawiera teraz metodę sort. Ponadto kompilator Javy zna już typy przekazanych parametrów, zatem i je możemy pominąć. Przyjrzyjmy się teraz praktycznym zastosowaniom wyrażeń lambda.

Interfejsy funkcyjne

Jak wyrażenia lambda wpisują się w system typów Javy? Każda lambda odpowiada danemu typowi, określonemu w interfejsie. Tak zwany interfejs funkcyjny musi zawierać dokładnie jedną deklarację metody abstrakcyjnej. Każde wyrażenie lambda danego typu zostanie powiązane z tą metodą. Jako że metody domyślne nie są abstrakcyjne, nic nie stoi na przeszkodzie by dodać je do interfejsu funkcyjnego.

Postać wyrażenia lambda może przyjąć dowolny interfejs, o ile zawiera tylko jedną metodę abstrakcyjną. By mieć pewność, że warunek ten jest spełniony, należy dodać do kodu adnotację @FunctionalInterface. Kompilator zna tę adnotację i zgłosi błąd kompilacji, gdy tylko spróbujemy dodać do interfejsu drugą deklarację metody abstrakcyjnej.

Przykład:

@FunctionalInterface
interface Converter<F, T> {
    T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert("123");
System.out.println(converted);    // 123

Warto pamiętać, że powyższy kod będzie działał prawidłowo także bez adnotacji @FunctionalInterface.

Referencje do metod i konstruktorów

Przytoczony wyżej przykład można uprościć jeszcze bardziej, posługując się referencjami do metod statycznych:

Converter<String, Integer> converter = Integer::valueOf;
Integer converted = converter.convert("123");
System.out.println(converted);   // 123

W Javie 8 referencje do metod i konstruktorów mogą być przekazywane za pomocą słowa kluczowego ::. Powyższy przykład ilustruje referencję do metody statycznej, jednak możliwe jest także tworzenie referencji do metod obiektów:

class Something {
    String startsWith(String s) {
        return String.valueOf(s.charAt(0));
    }
}
Something something = new Something();
Converter<String, String> converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted);    // "J"

Zobaczmy jak słowo kluczowe :: działa w przypadku konstruktorów. Na początek zdefiniujmy przykładowy komponent bean z różnymi konstruktorami:

class Person {
    String firstName;
    String lastName;

    Person() {}

    Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Następnie określimy interfejs PersonFactory służący do dodawania nowych osób:

interface PersonFactory<P extends Person> {
    P create(String firstName, String lastName);
}

Nie będziemy implementować fabryki ręcznie – zamiast tego połączymy wszystkie składniki, korzystając z odwołania do konstruktora:

PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");

Utworzyliśmy referencję do konstruktora Person za pomocą składni Person::new. Kompilator Javy automatycznie wybierze odpowiedni konstruktor o sygnaturze zgodnej z PersonFactory.create.

Zakres wyrażeń lambda

Odwoływanie się z wyrażeń lambda do zmiennych z zakresu zewnętrznego działa podobnie jak w przypadku obiektów anonimowych. Zmienne finalne są dostępne z lokalnego zasięgu zewnętrznego, pól instancji i zmiennych statycznych.

Dostęp do zmiennych lokalnych

Finalne zmienne lokalne można odczytać z poziomu zakresu zewnętrznego wyrażeń lambda:

final int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

W przeciwieństwie do obiektów anonimowych zmienna num nie musi być zadeklarowana jako finalna. Poniższy kod jest zatem prawidłowy:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);

stringConverter.convert(2);     // 3

By kod został skompilowany, wartość num musi być jednak finalna niejawnie. Kompilacja tego kodu nie powiedzie się:

int num = 1;
Converter<Integer, String> stringConverter =
        (from) -> String.valueOf(from + num);
num = 3;

Zabronione jest również nadpisywanie wartości num z poziomu wyrażenia lambda.

Dostęp do pól i zmiennych statycznych

W przeciwieństwie do zmiennych lokalnych, pola instancji i zmienne statyczne można zarówno odczytać jak i nadpisać z poziomu lambd. Podobnie działa to w obiektach anonimowych.

class Lambda4 {
    static int outerStaticNum;
    int outerNum;

    void testScopes() {
        Converter<Integer, String> stringConverter1 = (from) -> {
            outerNum = 23;
            return String.valueOf(from);
        };

        Converter<Integer, String> stringConverter2 = (from) -> {
            outerStaticNum = 72;
            return String.valueOf(from);
        };
    }
}

Dostęp do domyślnych metod interfejsów

Pamiętacie przykład obiektu formula przytoczony w pierwszej sekcji artykułu? Interfejs Formula definiuje domyślną metodę sqrt, do której można uzyskać dostęp z każdego egzemplarza, w tym z obiektu anonimowego. W przypadku lambd nie jest to możliwe.

Z poziomu wyrażenia lambda nie można uzyskać dostępu do metod domyślnych. Kompilacja poniższego kodu zakończy się niepowodzeniem:

Formula formula = (a) -> sqrt( a * 100);

Wbudowane interfejsy funkcyjne

API JDK 1.8 zawiera wiele wbudowanych interfejsów funkcyjnych. Niektóre z nich, np. Comparator czy Runnable, są nam dobrze znane ze starszych wersji Javy. Te istniejące już interfejsy zostały rozszerzone o obsługę lambd za pomocą adnotacji @FunctionalInterface.

API Javy 8 jest jednak bogate również w wiele nowych interfejsów, które mają za zadanie ułatwić nam pracę. Niektóre z nich wywodzą się z biblioteki Google Guava. Nawet jeśli jest wam ona znana, zwróćcie uwagę na to jak zaczerpnięte z niej interfejsy zostały rozszerzone dzięki zastosowaniu użytecznych rozszerzeń metod.

Predykaty

Predykaty to jednoargumentowe funkcje o wartości logicznej. Ich interfejs zawiera różne metody domyślne służące do łączenia predykatów w złożone operacje logiczne (koniunkcję, alternatywę, negację).

Predicate<String> predicate = (s) -> s.length() > 0;

predicate.test("foo");              // true
predicate.negate().test("foo");     // false

Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;

Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();

Funkcje

Funkcje przyjmują jeden argument i zwracają wynik. Kilka funkcji (np. compose, andThen) można połączyć w łańcuch za pomocą metod domyślnych.

Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);

backToString.apply("123");     // "123"

Dostawcy

Dostawcy zwracają wynik o określonym typie generycznym. W przeciwieństwie do funkcji dostawcy nie przyjmują argumentów.

Supplier<Person> personSupplier = Person::new;
personSupplier.get();   // new Person

Konsumenci

Konsumenci reprezentują operacje, jakie mają zostać wykonane na pojedynczym argumencie wejściowym.

Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));

Komparatory

Komparatory są nam dobrze znane z poprzednich wersji Javy. W Javie 8 interfejs ten został rozszerzony o metody domyślne.

Comparator<Person> comparator = (p1, p2) -> p1.firstName.compareTo(p2.firstName);

Person p1 = new Person("John", "Doe");
Person p2 = new Person("Alice", "Wonderland");

comparator.compare(p1, p2);             // > 0
comparator.reversed().compare(p1, p2);  // < 0

Wartości opcjonalne

Optional to nie interfejs funkcyjny, lecz sprytna klasa pomagająca uniknąć wyjątku NullPointerException. Odgrywa ona ważną rolę w kontekście kolejnej sekcji, zatem przyjrzyjmy się na czym polega jej działanie.

Egzemplarz klasy Optional to prosty kontener przechowujący wartość null lub inną niż null. Wyobraźmy sobie metodę, która może zwracać wynik o wartości innej niż null, a czasem może nie zwracać niczego. Zamiast wartości null, w Javie 8 zwracamy obiekt klasy Optional.

Optional<String> optional = Optional.of("bam");

optional.isPresent();           // true
optional.get();                 // "bam"
optional.orElse("fallback");    // "bam"

optional.ifPresent((s) -> System.out.println(s.charAt(0)));     // "b"

Strumienie

Strumień stanowi sekwencję elementów, na których może zostać wykonana co najmniej jedna operacja. Operacje strumieniowe mogą być pośrednie lub kończące. Operacje kończące zwracają wynik określnego typu, natomiast pośrednie zwracają sam strumień, co pozwala połączyć w łańcuch kilka wywołań metody z rzędu. Strumienie są tworzone na podstawie źródła, np. kolekcji takiej jak lista czy zbiór (słowniki nie są obsługiwane). Operacje strumieniowe mogą być wykonywane sekwencyjnie bądź równolegle.

Zachęcam do zapoznania się także z biblioteką Stream.js, javascriptowym przeniesieniem strumieni z Javy 8.

Na początek zobaczymy, jak działają strumienie sekwencyjne. Zaczniemy od utworzenia prostego źródła w postaci listy łańcuchów:

List<String> stringCollection = new ArrayList<>();
stringCollection.add("ddd2");
stringCollection.add("aaa2");
stringCollection.add("bbb1");
stringCollection.add("aaa1");
stringCollection.add("bbb3");
stringCollection.add("ccc");
stringCollection.add("bbb2");
stringCollection.add("ddd1");

Ponieważ kolekcje w Javie 8 są rozszerzone, strumienie można utworzyć w łatwy sposób, wywołując metodę Collection.stream() lub Collection.parallelStream(). W poniższych sekcjach opisuję najczęściej stosowane operacje strumieniowe.

Filter

Operacja filter przyjmuje predykat, na podstawie którego filtruje wszystkie elementy strumienia. To operacja pośrednia, dzięki czemu na wyniku możemy wykonać inną operację strumieniową (forEach). ForEach przyjmuje konsumenta, którego operacje mają zostać wykonane dla każdego elementu w przefiltrowanym strumieniu. To operacja kończąca. Typ zwrotny to void, dlatego nie można wywołać kolejnej operacji strumieniowej.

stringCollection
    .stream()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa2", "aaa1"

Sorted

Sorted to operacja pośrednia zwracająca posortowany widok strumienia. Elementy są sortowane w porządku naturalnym, chyba że przekażemy własny komparator.

stringCollection
    .stream()
    .sorted()
    .filter((s) -> s.startsWith("a"))
    .forEach(System.out::println);

// "aaa1", "aaa2"

Należy pamiętać, że operacja sorted tworzy jedynie posortowany widok strumienia, nie manipulując przy tym porządkiem elementów samej kolekcji źródłowej. Porządek elementów w stringCollection pozostaje bez zmian:

System.out.println(stringCollection);
// ddd2, aaa2, bbb1, aaa1, bbb3, ccc, bbb2, ddd1

Map

Pośrednia operacja map konwertuje każdy element na inny obiekt za pośrednictwem podanej funkcji. W poniższym przykładzie zamieniamy łańcuch na łańcuch zapisany wielkimi literami, lecz za pomocą tej operacji moglibyśmy też zmienić typ dowolnego obiektu na inny. Typ generyczny wynikowego strumienia zależy od typu generycznego przekazanej funkcji.

stringCollection
    .stream()
    .map(String::toUpperCase)
    .sorted((a, b) -> b.compareTo(a))
    .forEach(System.out::println);

// "DDD2", "DDD1", "CCC", "BBB3", "BBB2", "AAA2", "AAA1"

Match

Korzystając z różnych operacji dopasowania można sprawdzić, czy dany predykat odpowiada strumieniowi. Wszystkie tego typu operacje są kończące i zwracają wartość logiczną.

boolean anyStartsWithA =
    stringCollection
        .stream()
        .anyMatch((s) -> s.startsWith("a"));

System.out.println(anyStartsWithA);      // true

boolean allStartsWithA =
    stringCollection
        .stream()
        .allMatch((s) -> s.startsWith("a"));

System.out.println(allStartsWithA);      // false

boolean noneStartsWithZ =
    stringCollection
        .stream()
        .noneMatch((s) -> s.startsWith("z"));

System.out.println(noneStartsWithZ);      // true

Count

Count to operacja kończąca, która zwraca liczbę elementów strumienia jako wartość typu long.

long startsWithB =
    stringCollection
        .stream()
        .filter((s) -> s.startsWith("b"))
        .count();

System.out.println(startsWithB);    // 3

Reduce

Operacja kończąca, która dokonuje redukcji elementów strumienia przy użyciu podanej funkcji. Wynikiem jest kontener Optional zawierający zredukowaną wartość.

Optional<String> reduced =
    stringCollection
        .stream()
        .sorted()
        .reduce((s1, s2) -> s1 + "#" + s2);

reduced.ifPresent(System.out::println);
// "aaa1#aaa2#bbb1#bbb2#bbb3#ccc#ddd1#ddd2"

Strumienie równoległe

Jak już wspomnieliśmy, strumienie mogą być sekwencyjne lub równoległe. Operacje na strumieniach sekwencyjnych są jednowątkowe, natomiast operacje na strumieniach równoległych wykonywane są w kilku wątkach jednocześnie.

Poniższy przykład pokazuje, jak łatwo można zwiększyć wydajność aplikacji dzięki zastosowaniu strumieni równoległych.

Najpierw utworzymy dużą listę składającą się z różnych elementów:

int max = 1000000;
List<String> values = new ArrayList<>(max);
for (int i = 0; i < max; i++) {
    UUID uuid = UUID.randomUUID();
    values.add(uuid.toString());
}

Teraz sprawdzimy, ile czasu zajmie posortowanie strumienia tej kolekcji.

Sortowanie sekwencyjne w Javie 8

long t0 = System.nanoTime();

long count = values.stream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("sequential sort took: %d ms", millis));

// sortowanie sekwencyjne zajęło 899 ms

Sortowanie równoległe w Javie 8

long t0 = System.nanoTime();

long count = values.parallelStream().sorted().count();
System.out.println(count);

long t1 = System.nanoTime();

long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);
System.out.println(String.format("parallel sort took: %d ms", millis));

// sortowanie równoległe zajęło 472 ms

Jak widać, różnice pomiędzy tymi dwoma fragmentami kodu są minimalne, lecz sortowanie równoległe jest o około 50% szybsze. By z niego skorzystać, wystarczy tylko zmienić stream() na parallelStream().

Słowniki

Jak zdążyliśmy już powiedzieć, słowniki nie obsługują strumieni bezpośrednio. W samym interfejsie Map nie ma metody stream(), jednak możemy utworzyć specjalne strumienie zawierające klucze, wartości bądź pozycje słownika za pomocą metod map.keySet().stream(), map.values().stream() i map.entrySet().stream().

Ponadto słowniki obsługują wiele nowych przydatnych metod ułatwiających wykonywanie najczęstszych zadań.

Map<Integer, String> map = new HashMap<>();

for (int i = 0; i < 10; i++) {
    map.putIfAbsent(i, "val" + i);
}

map.forEach((id, val) -> System.out.println(val));

Powyższy kod powinien być zrozumiały: putIfAbsent uwalnia nas od pisania dodatkowych instrukcji sprawdzających czy wartość wynosi null; forEach przyjmuje konsumenta, by wykonać operacje dla każdej wartości słownika.

Oto przykład pokazujący, jak wykonać na słowniku operacje za pomocą funkcji:

map.computeIfPresent(3, (num, val) -> val + num);
map.get(3);             // val33

map.computeIfPresent(9, (num, val) -> null);
map.containsKey(9);     // false

map.computeIfAbsent(23, num -> "val" + num);
map.containsKey(23);    // true

map.computeIfAbsent(3, num -> "bam");
map.get(3);             // val33

Następnie możemy usunąć pozycję przypisaną kluczowi, o ile jest on obecnie przyporządkowany do podanej wartości:

map.remove(3, "val3");
map.get(3);             // val33

map.remove(3, "val33");
map.get(3);             // null

Inna pomocna metoda:

map.getOrDefault(42, "nie znaleziono");  // nie znaleziono

Scalanie pozycji słownika to nic trudnego:

map.merge(9, "val9", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9

map.merge(9, "concat", (value, newValue) -> value.concat(newValue));
map.get(9);             // val9concat

Funkcja scalająca albo doda parę klucz/wartość do słownika, jeśli klucz nie miał przypisanej wartości, albo zmieni istniejącą już wartość.

API daty i czasu

Java 8 zawiera zupełnie nowy interfejs API daty i czasu w pakiecie java.time. Jest on porównywalny z biblioteką Joda-Time, lecz to nie to samo. Poniższe fragmenty ilustrują najważniejsze elementy nowego API Date.

Zegar

Zegary umożliwiają dostęp do aktualnej daty i czasu. Uwzględniają strefę czasową, dlatego można nimi zastąpić System.currentTimeMillis(), by sprawdzić liczony w milisekundach czas, jaki upłynął od początku epoki Uniksa. Taki bieżący punkt na linii czasu jest także reprezentowany przez klasę Instant. Z jej pomocą można utworzyć historyczne obiekty java.util.Date.

Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();

Instant instant = clock.instant();
Date legacyDate = Date.from(instant);   // legacy java.util.Date

Strefy czasowe

Strefy czasowe oznaczone są przez identyfikator ZoneId. Są łatwo dostępne za pośrednictwem statycznych metod wytwórczych. Strefy czasowe określają wartość przesunięcia potrzebną do konwersji pomiędzy instancjami klasy Instant a lokalnymi datami i godzinami.

System.out.println(ZoneId.getAvailableZoneIds());
// wyświetla wszystkie dostępne identyfikatory stref czasowych

ZoneId zone1 = ZoneId.of("Europe/Berlin");
ZoneId zone2 = ZoneId.of("Brazil/East");
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());

// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]

Czas lokalny

Czas lokalny (LocalTime) reprezentuje czas bez strefy czasowej, np. 10 czy 17:30:15. W poniższym przykładzie tworzymy dwa czasy lokalne dla stref czasowych zdefiniowanych powyżej. Następnie porównamy je ze sobą i obliczymy mierzoną w godzinach i minutach różnicę, jaka je dzieli.

LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);

System.out.println(now1.isBefore(now2));  // false

long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);

System.out.println(hoursBetween);       // -3
System.out.println(minutesBetween);     // -239

Klasa LocalTime dostarcza wielu metod wytwórczych upraszczających tworzenie nowych instancji, w tym metodę do parsowania łańcuchów oznaczających czas.

LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late);       // 23:59:59

DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedTime(FormatStyle.SHORT)
        .withLocale(Locale.GERMAN);

LocalTime leetTime = LocalTime.parse("13:37", germanFormatter);
System.out.println(leetTime);   // 13:37

Data lokalna

Data lokalna (LocalDate) reprezentuje konkretną datę, np. 2014-03-11. To klasa niemodyfikowalna, działająca analogicznie do LocalTime. Na poniższym przykładzie demonstrujemy, jak obliczać nowe daty przez dodawanie bądź odejmowanie dni, miesięcy lub lat. Należy pamiętać, że każda manipulacja zwraca nową instancję klasy.

LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);

LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek);    // FRIDAY

Przetworzenie daty lokalnej z łańcucha jest równie łatwe co przetworzenie czasu lokalnego:

DateTimeFormatter germanFormatter =
    DateTimeFormatter
        .ofLocalizedDate(FormatStyle.MEDIUM)
        .withLocale(Locale.GERMAN);

LocalDate xmas = LocalDate.parse("24.12.2014", germanFormatter);
System.out.println(xmas);   // 2014-12-24

LocalDateTime

LocalDateTime to połączenie daty i czasu ze wspomnianych wyżej klas w jedną instancję. Obiekt LocalDateTime jest niemodyfikowalny i działa podobnie jak LocalTime i LocalDate. Korzystając z odpowiednich metod, można pobrać dane przechowywane w polach LocalDateTime:

LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);

DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek);      // WEDNESDAY

Month month = sylvester.getMonth();
System.out.println(month);          // DECEMBER

long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay);    // 1439

Po dodaniu informacji o strefie czasowej, obiekt LocalDateTime można zamienić na typ Instant. Ten z kolei da się łatwo przekonwertować na datę starego typu java.util.Date.

Instant instant = sylvester
        .atZone(ZoneId.systemDefault())
        .toInstant();

Date legacyDate = Date.from(instant);
System.out.println(legacyDate);     // Wed Dec 31 23:59:59 CET 2014

Formatowanie połączonej daty i czasu wykonuje się identycznie co formatowanie daty bądź czasu z osobna. Możemy skorzystać z predefiniowanego formatu lub utworzyć nowy na podstawie własnego wzorca.

DateTimeFormatter formatter =
    DateTimeFormatter
        .ofPattern("MMM dd, yyyy - HH:mm");

LocalDateTime parsed = LocalDateTime.parse("Nov 03, 2014 - 07:13", formatter);
String string = formatter.format(parsed);
System.out.println(string);     // Nov 03, 2014 - 07:13

W przeciwieństwie do java.text.NumberFormat nowy formater DateTimeFormatter jest niemodyfikowalny i bezpieczny pod kątem działania wątków.

Szczegółowe informacje na temat składni wzorców można znaleźć tutaj.

Adnotacje

W Javie 8 adnotacje są powtarzalne. By zrozumieć na czym to polega, przejdźmy prosto do przykładu.

Zaczynamy od zdefiniowania adnotacji opakowującej, która przechowuje tablicę właściwych adnotacji:

@interface Hints {
    Hint[] value();
}

@Repeatable(Hints.class)
@interface Hint {
    String value();
}

W Javie 8 można korzystać z wielu adnotacji tego samego typu – należy w tym celu zadeklarować adnotację @Repeatable.

Wariant 1: adnotacje kontenera (stara szkoła)

@Hints({@Hint("hint1"), @Hint("hint2")})
class Person {}

Wariant 2: adnotacje powtarzalne (nowa szkoła)

@Hint("hint1")
@Hint("hint2")
class Person {}

W przypadku wariantu 2. kompilator Javy dodaje adnotację @Hints niejawnie „pod maską”. To ważne, by móc korzystać z adnotacji za pomocą refleksji.

Hint hint = Person.class.getAnnotation(Hint.class);
System.out.println(hint);                   // null

Hints hints1 = Person.class.getAnnotation(Hints.class);
System.out.println(hints1.value().length);  // 2

Hint[] hints2 = Person.class.getAnnotationsByType(Hint.class);
System.out.println(hints2.length);          // 2

Mimo iż nie zadeklarowaliśmy adnotacji @Hints w klasie Person, to możemy ją odczytać poprzez metodę getAnnotation(Hints.class) . Wygodniej jest to jednak zrobić za pośrednictwem metody getAnnotationsByType, która daje nam dostęp do wszystkich adnotacji @Hint.

Ponadto w Javie 8 z adnotacji można korzystać na dwa nowe sposoby:

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

Skończyłem lekturę — co dalej?

W tym miejscu kończy się mój przewodnik po Javie. Jeśli chcielibyście dowiedzieć się czegoś więcej o klasach i innych składnikach API JDK 8, zachęcam do zapoznania się z narzędziem JDK8 API Explorer mojego autorstwa. Pomoże wam ono odkryć nowe klasy i kryjące się w ostatniej wersji Javy perełki, między innymi Arrays.parallelSort, StampedLock czy CompletableFuture.

W nawiązaniu do tego przewodnika opublikowałem też na blogu kilka artykułów, które mogą was zainteresować:

Dzięki za lekturę!

Autor: Benjamin Winterberg

Źródło: https://github.com/winterbe/java8-tutorial

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji MIT