Jedną z nowości wprowadzonych w Javie 8 jest nowe API związane z obsługą dat i czasu, znane też jako JSR-310. Ta długo oczekiwana zmiana pozwala na realne korzystanie z typów czasowych, bez stosowania obejść lub dodatkowych bibliotek.
Pomimo tego, że nie wszystkie projekty korzystają jeszcze z dobrodziejstw najnowszej Javy, jest to zdecydowanie jedna z ważniejszych rzeczy, na której powinnaś się skupić w nauce. Api do dat i czasu na szczęście jest łatwe do zrozumienia, logiczne i w dużej mierze podobne do biblioteki Joda, z którą zapewne się już spotkałaś, lub o niej słyszałaś.
Klasy związane ze strefami czasowymi
Te klasy najbliżej odpowiadają temu, co już najprawdopodobniej znasz — klasie java.util.Date oraz klasie DateTime z biblioteki joda time. Reprezentują one pewien moment czasu obserwowany w pewnych okolicznościach (np. z określonej lokalizacji geograficznej — strefy czasowej).
Do najwaznijeszych klas z tej grupy należą:
ZonedDateTime — data i czas powiązane z konkretną strefą czasową, rozumianą jako przybliżoną lokalizację geograficzną (np. Europa/Warszawa). Takie powiązanie pozwala także na uwzględnianie kwestii takich jak czas letni/zimowy itp.
ZoneId — identyfikator strefy czasowej jako rejonu geograficznego (np. ZoneId.of(“Europe/Paris”) zwróci identyfikator odpowiadający strefie czasowej obowiązującej w Paryżu)
OffsetDateTime — data i czas powiązane z konkretną strefą czasową, ale rozumianą jako przesunięcie czasu. W przeciwieństwie do klasy ZonedDateTime, tutaj mamy tylko informację o przesunięciu czasowym, nie jesteśmy więc w stanie uzyskać informacji o czasie letnim/zimowym czy przybliżonej lokalizacji geograficznej. Lub formalniej — reprezentacja czasu zgodna ze standardem ISO-8601
ZoneOffset — przesunięcie czasu związane ze strefą czasową opisane w godzinach
Te klasy, w szczególności ZonedDateTime, powinny być pierwszymi z rozważanych, gdy zastanawiasz się jak przechowywać informacje o czasie w swoim projekcie.
Jeśli jednak w systemie potrzebujesz przechowywać informacje o momencie konkretnego zdarzenia ogólnie, bez informacji o strefie czasowej czy liczbach widniejących na zegarku gdziekolwiek, przyjrzyj się bliżej typowi Instant opisanemu poniżej.
Klasy reprezentujące lokalny czas lub czas ogólny bez informacji o strefie czasowej
Ta kategoria klas nie miała bezpośrednich odpowiedników w dawnym API Javy, może być jednak znana z np. języka SQL lub biblioteki Joda.
LocalTime — reprezentuje czas, bez powiązania go z konkretną strefą czasową czy lokalizacją geograficzną czy nawet datą. Tak, jakby ktoś powiedział “spotkajmy się o 17” — sama “godzina 17” pozbawiona kontekstu (dzisiejszy dzień, kraj, w którym się znajdujesz) jest nieprecyzyjna
LocalDate — reprezentuje datę, bez powiązania jej z konkretną strefą czasową czy lokalizacją geograficzną. Podobnie jak w przypadku LocalTime, pozwala na precyzyjne określenie momentu w czasie tylko jesli umieścimy ją w określonym kontekście (np. mówimy o południu danego dnia i jesteśmy w takim i takim miejscu)
LocalDateTime — ta klasa łączy dwie powyższe — datę oraz czas, w rozumieniu lokalnym dla obserwatora. Także nie posiada informacji o strefie czasowej, można ją porównać z obiektową reprezentacją ciągu znaków “01.01.2015 12:20:00” — nie wiemy czy jest to czas GMT, czy też może obserwowany w Portugalii.
Instant — ta klasa wyróżnia się na tle pozostałych tym, że reprezentuje konkretny i jednoznacznie określony punkt w czasie (z dokładnością do nanosekundy — w przeciwieństwie do milisekund w przypadku java.util.Date). Drugą ważną cechą jest to, że nie jest ona powiązana z konceptem dni czy lat, a jedynie z uniwersalnym czasem, tzw. UTC . W skrócie przechowuje ona wewnętrznie liczbę sekund (z dokładnością do nanosekund) od pewnego ustalonego punktu w czasie (1 stycznia 1970 roku — tzw. Epoch time). Aby zamienić ją na jakąkolwiek czytelna reprezentację, potrzebujemy dołączyć informacje o strefie czasowej (także zamieniając na czas lokalny). Najlepiej nadaje się do przechowywania informacji o czasie, które będą przetwarzane tylko przez system (np. czas jakiegoś zdarzenia itp).
Każda z tych klas ma inne zastosowanie (o czym nieco dalej), oczywiście możliwa jest także konwersja pomiędzy nimi (o czym też sobie opowiemy)
Odstępy czasowe (klasy Duration, Period)
JSR-310 wprowadza pojęcie odstępu czasu jako czasu, który upłynął pomiędzy momentami A i B. Służą do tego dwie klasy — Duration i Period — które różnią się jedynie jednostkami (pozwalają reprezentować odpowiednio jednostki czasu i np. miesiące czy lata).
Odstepy te możemy potem wykorzystywać w operacjach na datach (np. odejmowanie czy dodawanie), możemy także obliczać ‘odstęp’ pomiędzy określonymi punktami w czasie w wybranych jednostkach (za pomocą metody Duration.between(…) ).
O operacjach więcej powiemy sobie poniżej, na tą chwilę warto znać metody pozwalające tworzyć odstępy o określonej długości, jak np:
Duration.ofDays(5) //odstęp 5 dni
Duration.ofHours(2) //odstęp 2 godzin
Rozróżnienie na Duration i Period wynika z prostego faktu — wszystkie długości wyrażane poprzez Duration, mają swoją reprezentację w podstawowych jednostkach czasu (czyli np. w sekundach), podczas gdy te wyrażane przez Period (miesiąc, rok, wiek, milenium) mogą mieć różną długość realną (przez różną ilość dni w miesiącach, lata przestępne itp).
Z tego powodu nie zawsze można wykorzystywać wszystkie jednostki w operacjach arytmetycznych na datach! Ale o tym więcej za moment.
Operacje na datach i ich porównywanie
Bardzo często mamy potrzebę porównania dat lub wykonania operacji na nich, takich jak np. odjęcie pewnego okresu czasu. Wcześniej czasem mogły być z tym problemy, na szczęście nowe API wprowadza w tym aspekcie wiele usprawnień.
Aby dodać lub odjąć pewien okres czasu, możemy użyć poniższej konstrukcji:
ZonedDateTime zonedDateTime = ZonedDateTime.now();
ZonedDateTime newZonedDateTime = zonedDateTime.minus(Period.ofDays(4));
ZonedDateTime newerZonedDateTime = zonedDateTime.plus(Period.ofDays(4));
Co bardzo ważne, obiekty te są niezmienne (Immutable), co oznacza, że każda taka operacja zwraca nową instancję obiektu, a nie modyfikuje poprzedniej.
Porównywanie obiektów czasowych wygląda podobnie jak dotychczas:
ZonedDateTime zonedDateTimeOne = ... ;
ZonedDateTime zonedDateTimeTwo = ... ;
if (zonedDateTimeOne.isAfter(zonedDateTimeTwo)) {
//pierwsza data jest po drugiej
}
Inne operacje
Inne operacje, o których warto wiedzieć to z pewnością metody statyczne now(), które występują we wszystkich obiektach API odnoszących się do daty i czasu, np:
ZonedDateTime.now();
Instant.now();
LocalDate.now(); //w przypadku klas Local* używana jest domyślna strefa czasowa
Kolejną metodą jest toLocalDate(), toLocalTime(), toInstant() i podobne (są one dostępne w zalezności od konkretnych implementacji).
Klasa Instant ma dodatkowo metody atZone(ZoneId) oraz atOffset(ZoneOffset), które pozwalają zamienić obiekt typu Instant na reprezentację konkretnej daty i godziny w określonej strefie czasowej.
Wszystkie metody podsumowuje poniższa tabelka:
Metoda | Instant | LocalTime | LocalDateTime | OffsetDateTime | ZonedDateTime | Typ zwracany | Opis |
---|---|---|---|---|---|---|---|
(statyczna) now() | (różne) | Zwraca instancje danego typu opisującą ‘teraz’ | |||||
toInstant() | Instant | Zwraca obiekt typu Instant reprezentujący ten sam moment | |||||
toLocalDateTime() | LocalDateTime | Zwraca obiekt reprezentujący lokalne datę i czas w tym samym momencie | |||||
toLocalTime() | LocalTime | Zwraca obiekt reprezentujący lokalny czas w tym samym momencie | |||||
toLocalDate() | LocalDate | Zwraca obiekt reprezentujący lokalną datę w tym samym momencie | |||||
atZone() | ZonedDateTime | Zwraca obiekt reprezentujący określony moment lub lokalny czas jako ZonedDateTime | |||||
atOffset() | OffsetDateTime | Zwraca obiekt reprezentujący określony moment lub lokalny czas jako OffsetDateTime |
Którą reprezentację wybrać
Zamiast jednej klasy wcześniej mamy teraz kilka różnych opcji pozwalających reprezentować datę i czas — przyjrzyjmy się więc na chwilę, jakie są między nimi różnice i którą implementację wybrać w jakiej sytuacji.
Instant — ta klasa najlepiej nadaje się do reprezentowania czasu w sposób, który będzie przetwarzany przez system i nie będzie wyświetlany użytkownikom końcowym. Dobrym przykładem są np. systemy, których elementy komunikują się ze sobą wewnętrznie albo zapisują informacje służące do audytu — ważny jest dokładny punkt w czasie, a nie jego reprezentacja w określonej strefie czasowej.
ZonedDateTime — używamy wszędzie tam, gdzie istotna jest data i godzina z punktu widzenia praktycznego (czyli np. wyświetlenie jej użytkownikowi, porównanie z innymi, ew konwersja) — dobrym przykładem może być aplikacja do zarządzania kalendarzem — w razie potrzeby taką datę możemy skonwertować do innej strefy czasowej. Ta klasa jest najlepszym wyborem dla ogólnego przypadku, choć czasem wygodniejsze może być przechowywanie osobno strefy czasowej użytkownika i punktu w czasie (w formie obiektu typu Instant) i łączenie obu dopiero wyświetlając.
OffsetDateTime — zastosowanie ma podobne, jak ZonedDateTime, ale w praktyce używana jest w sytuacji, w której nie mamy dokładnej informacji o strefie czasowej powiązanej z lokalizacją, ale mamy informacje o przesunięciu czasowym (przykładem może być np. parsowanie dat ze Stringów — często taka reprezentacja zawiera strefę czasową w reprezentacji np. +03:00). Z tego powodu najczęściej spotkasz się z tą reprezentacją w systemach, które np. importują dane w postaci plików.
LocalDateTime — tą klasę wybierzemy, jeśli system jest ‘lokalny’, tzn. działa tylko w określonym budynku, urządzeniu (np. aplikacja mobilna) itp, a informacja, którą opisujemy, nie będzie udostępniana poza rzeczonym systemem. Realnie praktyczne zastosowania są dość ograniczone i często sprowadzają się do wyświetlania — np. poprzez konwersję innych typów do LocalDateTime w celu ich prezentacji użytkownikowi. Można wykorzystać do prowadzenia jakiejś formy ‘dziennika zdarzeń’ przez fizyczną osobę, ale w tylko nieco bardziej ogólnym przypadku lepszym wyborem będzie już ZonedDateTime. Tej klasy użyjemy także w sytuacji, kiedy nie mamy informacji o strefie czasowej użytkownika, a mamy podaną przez niego datę i godzinę.
To oczywiście tylko wskazówki, z uwagi na określone wymagania projektu może się okazać, że inna implementacja lepiej spełnia Twoje potrzeby. Najważniejsze, żeby znać zalety i wady każdej z implementacji aby móc podjąć świadomą decyzję.
Mapowanie na odpowiedniki SQL
Większość projektów, z jakimi się spotkasz, będzie wykorzystywała relacyjne bazy danych do przechowywania informacji. Poniższa tabelka podsumowuje mapowanie pomiędzy typami w SQL a klasami w DateTime API.
ANSI SQL | Java SE 8 |
DATE | LocalDate |
TIME | LocalTime |
TIMESTAMP | LocalDateTime |
TIME WITH TIMEZONE | OffsetTime |
TIMESTAMP WITH TIMEZONE | OffsetDateTime |
Zwróć uwagę, że standardowe typy są mapowane na Local*. Należy o tym pamiętać, w przeciwnym razie mapując je np. na obiekty klasy ZonedDateTime przyjęta strefa czasowa będzie domyślną strefą JVM, co może nie odpowiadać rzeczywistości.
Wyświetlanie i parsowanie
W nowym API Javy dodana została klasa DateTimeFormatter, która funkcjonalnością jest zbliżona do SimpleDateFormat, która istniała wcześniej. Ogólne założenie jest proste — inicjalizujemy formater podając mu wzorzec daty, jakiego oczekujemy lub jaki chcemy wypisać, po czym przekazujemy mu obiekt daty lub wczytujemy dane do takiego obiektu.
Aby sformatować datę do wybranego formatu, wystarczy poniższy fragment kodu:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
String text = date.toString(formatter);
Z kolei wczytanie daty o zadanym formacie z ciągu znaków do obiektu wygląda nastepująco:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
LocalDate date = LocalDate.parse(text, formatter);
Zwróć uwagę, że metoda parse jest metodą nie samego formatera, ale konkretnych klas reprezentujących datę. Dzięki temu możesz od razu utworzyć obiekt o interesującym Cię typie (np. LocalDate), bez konieczności konwersji.
Dawne API i różnice
Poza dużo większym wyborem jeśli chodzi o możliwe implementacje, zmieniła się cała koncepcja- zobaczmy najważniejsze zmiany ‘ogólne’.
- Obiekty stały się niezmienne (Immutable), operacje na nich zwracają nową instancję zamiast modyfikować bieżącą — ta pozornie nieistotna zmiana pozwala tworzyć poprawne obiekty transferowe (które nie powinny mieć możliwości modyfikacji), ułatwia też utrzymanie kodu (np. pola z modyfikatorem final nie zmienią wartości w trakcie działania aplikacji)
- Obiekty pozwalają reprezentować samą datę lub samą godzinę, także bez powiązanej informacji o strefie czasowej, poza lepszym odzwierciedleniem rzeczywistości, pozwala to także dokładniej mapować typy
- Czas jest reprezentowany z nanosekundową dokładnością (wcześniej: milisekundową) — raczej nieistotne dla użytkowników końcowych, ważniejsze z punktu widzenia kolejkowania zdarzeń następujących krótko po sobie, obliczania małych odstępów czasu itp.
- Obiekt reprezentujący punkt w czasie odłączony od koncepcji kalendarza, dat i godzin
- Wprowadzono pojęcie czasu jako odstępu pomiędzy dwoma punktami w czasie
- API jest Domain-Driven, tzn. powstało poprzez mapowanie rzeczywistości na klasy
- Nowe API jest niezależne od kalendarza — daty mogą być reprezentowane nie tylko w znanym nam kalendarzu, ale także np. w Buddyjskim lub Japońskim — nie jest to problemem
Zmian jest oczywiście dużo więcej, te powyższe to tylko te subiektywnie najważniejsze z punktu widzenia praktycznego.
Podsumowanie
API związane z datami, które wprowadzono w Javie 8 zawiera sporo nowości, ale są to zdecydowanie potrzebne i przydatne zmiany. Część rozwiązań może wydawać się nieintuicyjna, ale są one przemyślane i mają sens, a to że działają tak a nie inaczej najczęściej podyktowane jest jakimiś dziwnymi rzeczywistymi przypadkami, które muszą modelować.
Nie zmienia to faktu, że znajomość tego API i jego poprawne użycie to absolutnie podstawy, które na dodatek mogą znacznie ułatwić Ci pracę i pozwolą uniknąć wielu potencjalnych problemów czy pomyłek.