„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ć:
- Java 8 Stream Tutorial
- Java 8 Nashorn Tutorial
- Java 8 Concurrency Tutorial: Threads and Executors
- Java 8 Concurrency Tutorial: Synchronization and Locks
- Java 8 Concurrency Tutorial: Atomic Variables and ConcurrentMap
- Java 8 API by Example: Strings, Numbers, Math and Files
- Avoid Null Checks in Java 8
- Fixing Java 8 Stream Gotchas with IntelliJ IDEA
- Using Backbone.js with Java 8 Nashorn
Dzięki za lekturę!