#Niezbędnik Juniora. Obsługa dat i czasu

By 7 August 2015 Niezbędnik Juniora

Jed­ną z nowoś­ci wprowad­zonych w Javie 8 jest nowe API związane z obsługą dat i cza­su, znane też jako JSR-310. Ta dłu­go oczeki­wana zmi­ana pozwala na realne korzys­tanie z typów cza­sowych, bez stosowa­nia obe­jść lub dodatkowych bibliotek.

Pomi­mo tego, że nie wszys­tkie pro­jek­ty korzys­ta­ją jeszcze z dobrodziejstw najnowszej Javy, jest to zde­cy­dowanie jed­na z ważniejszych rzeczy, na której powin­naś się skupić w nauce. Api do dat i cza­su na szczęś­cie jest łatwe do zrozu­mienia, log­iczne i w dużej mierze podob­ne do bib­liote­ki 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 odpowiada­ją temu, co już najpraw­dopodob­niej znasz — klasie java.util.Date oraz klasie Date­Time z bib­liote­ki joda time. Reprezen­tu­ją one pewien moment cza­su obser­wowany w pewnych okolicznoś­ci­ach (np. z określonej lokaliza­cji geograficznej — stre­fy czasowej).

Do najwazni­jeszych klas z tej grupy należą:

Zoned­Date­Time — data i czas pow­iązane z konkret­ną stre­fą cza­sową, rozu­mi­aną jako przy­bliżoną lokaliza­cję geograficzną (np. Europa/Warszawa). Takie pow­iązanie pozwala także na uwzględ­ni­an­ie kwestii takich jak czas letni/zimowy itp.

ZoneId — iden­ty­fika­tor stre­fy cza­sowej jako rejonu geograficznego (np. ZoneId.of(“Europe/Paris”) zwró­ci iden­ty­fika­tor odpowiada­ją­cy stre­fie cza­sowej obow­iązu­jącej w Paryżu)

Off­set­Date­Time — data i czas pow­iązane z konkret­ną stre­fą cza­sową, ale rozu­mi­aną jako prze­sunię­cie cza­su. W prze­ci­wieńst­wie do klasy Zoned­Date­Time, tutaj mamy tylko infor­ma­cję o prze­sunię­ciu cza­sowym, nie jesteśmy więc w stanie uzyskać infor­ma­cji o cza­sie letnim/zimowym czy przy­bliżonej lokaliza­cji geograficznej. Lub for­mal­niej — reprezen­tac­ja cza­su zgod­na ze stan­dar­d­em ISO-8601

Zone­Off­set — prze­sunię­cie cza­su związane ze stre­fą cza­sową opisane w godzinach

Te klasy, w szczegól­noś­ci Zoned­Date­Time, powin­ny być pier­wszy­mi z rozważanych, gdy zas­tanaw­iasz się jak prze­chowywać infor­ma­c­je o cza­sie w swoim projekcie.

Jeśli jed­nak w sys­temie potrze­bu­jesz prze­chowywać infor­ma­c­je o momen­cie konkret­nego zdarzenia ogól­nie, bez infor­ma­cji o stre­fie cza­sowej czy liczbach wid­nieją­cych na zegarku gdziekol­wiek, przyjrzyj się bliżej typowi Instant opisane­mu poniżej.

Klasy reprezentujące lokalny czas lub czas ogólny bez informacji o strefie czasowej

Ta kat­e­go­ria klas nie miała bezpośred­nich odpowied­ników w dawnym API Javy, może być jed­nak znana z np. języ­ka SQL lub bib­liote­ki Joda.

Local­Time — reprezen­tu­je czas, bez pow­iąza­nia go z konkret­ną stre­fą cza­sową czy lokaliza­cją geograficzną czy nawet datą. Tak, jak­by ktoś powiedzi­ał “spotka­jmy się o 17” — sama “godz­i­na 17” pozbaw­iona kon­tek­stu (dzisiejszy dzień, kraj, w którym się zna­j­du­jesz) jest nieprecyzyjna

Local­Date — reprezen­tu­je datę, bez pow­iąza­nia jej z konkret­ną stre­fą cza­sową czy lokaliza­cją geograficzną. Podob­nie jak w przy­pad­ku Local­Time, pozwala na pre­cyzyjne określe­nie momen­tu w cza­sie tylko jes­li umieścimy ją w określonym kon­tekś­cie (np. mówimy o połud­niu danego dnia i jesteśmy w takim i takim miejscu)

Local­Date­Time — ta klasa łączy dwie powyższe — datę oraz czas, w rozu­mie­niu lokalnym dla obser­wa­to­ra. Także nie posi­a­da infor­ma­cji o stre­fie cza­sowej, moż­na ją porów­nać z obiek­tową reprezen­tacją ciągu znaków “01.01.2015 12:20:00” — nie wiemy czy jest to czas GMT, czy też może obser­wowany w Portugalii.

Instant — ta klasa wyróż­nia się na tle pozostałych tym, że reprezen­tu­je konkret­ny i jed­noz­nacznie określony punkt w cza­sie (z dokład­noś­cią do nanosekundy — w prze­ci­wieńst­wie do milisekund w przy­pad­ku java.util.Date). Drugą ważną cechą jest to, że nie jest ona pow­iązana z kon­ceptem dni czy lat, a jedynie z uni­w­er­sal­nym cza­sem, tzw. UTC . W skró­cie prze­chowu­je ona wewnętrznie liczbę sekund (z dokład­noś­cią do nanosekund) od pewnego ustalonego punk­tu w cza­sie (1 sty­cz­nia 1970 roku — tzw. Epoch time). Aby zamienić ją na jakąkol­wiek czytel­na reprezen­tację, potrze­bu­je­my dołączyć infor­ma­c­je o stre­fie cza­sowej (także zamieni­a­jąc na czas lokalny). Najlepiej nada­je się do prze­chowywa­nia infor­ma­cji o cza­sie, które będą przetwarzane tylko przez sys­tem (np. czas jakiegoś zdarzenia itp).

Każ­da z tych klas ma inne zas­tosowanie (o czym nieco dalej), oczy­wiś­cie możli­wa jest także kon­wer­s­ja pomiędzy nimi (o czym też sobie opowiemy)

Odstępy czasowe (klasy Duration, Period)

JSR-310 wprowadza poję­cie odstępu cza­su jako cza­su, który upłynął pomiędzy momen­ta­mi A i B. Służą do tego dwie klasy — Dura­tion i Peri­od — które różnią się jedynie jed­nos­tka­mi (pozwala­ją reprezen­tować odpowied­nio jed­nos­t­ki cza­su i np. miesiące czy lata).

Odstepy te może­my potem wyko­rzysty­wać w oper­ac­jach na dat­ach (np. ode­j­mowanie czy dodawanie), może­my także obliczać ‘odstęp’ pomiędzy określony­mi punk­ta­mi w cza­sie w wybranych jed­nos­tkach (za pomocą metody Duration.between(…) ).

O oper­ac­jach więcej powiemy sobie poniżej, na tą chwilę warto znać metody pozwala­jące tworzyć odstępy o określonej dłu­goś­ci, jak np:

Duration.ofDays(5) //odstęp 5 dni
Duration.ofHours(2) //odstęp 2 godzin

Rozróżnie­nie na Dura­tion i Peri­od wyni­ka z prostego fak­tu — wszys­tkie dłu­goś­ci wyrażane poprzez Dura­tion, mają swo­ją reprezen­tację w pod­sta­wowych jed­nos­tkach cza­su (czyli np. w sekun­dach), pod­czas gdy te wyrażane przez Peri­od (miesiąc, rok, wiek, mile­ni­um) mogą mieć różną dłu­gość real­ną (przez różną ilość dni w miesią­cach, lata przestęp­ne itp).

Z tego powodu nie zawsze moż­na wyko­rzysty­wać wszys­tkie jed­nos­t­ki w oper­ac­jach aryt­mety­cznych na dat­ach! Ale o tym więcej za moment.

Operacje na datach i ich porównywanie

Bard­zo częs­to mamy potrze­bę porów­na­nia dat lub wyko­na­nia oper­acji na nich, takich jak np. odję­cie pewnego okre­su cza­su. Wcześniej cza­sem mogły być z tym prob­le­my, na szczęś­cie nowe API wprowadza w tym aspekcie wiele usprawnień.

Aby dodać lub odjąć pewien okres cza­su, może­my użyć poniższej konstrukcji:

ZonedDateTime zonedDateTime = ZonedDateTime.now();
ZonedDateTime newZonedDateTime = zonedDateTime.minus(Period.ofDays(4));
ZonedDateTime newerZonedDateTime = zonedDateTime.plus(Period.ofDays(4));

Co bard­zo ważne, obiek­ty te są niezmi­enne (Immutable), co oznacza, że każ­da taka oper­ac­ja zwraca nową instancję obiek­tu, a nie mody­fiku­je poprzedniej.

Porówny­wanie obiek­tów cza­sowych wyglą­da podob­nie jak dotychczas:

ZonedDateTime zonedDateTimeOne = ... ;
ZonedDateTime zonedDateTimeTwo = ... ;
if (zonedDateTimeOne.isAfter(zonedDateTimeTwo)) {
    //pierwsza data jest po drugiej
}

Inne operacje

Inne oper­ac­je, o których warto wiedzieć to z pewnoś­cią metody staty­czne now(), które wys­tępu­ją we wszys­t­kich obiek­tach API odnoszą­cych się do daty i cza­su, np:

ZonedDateTime.now();
Instant.now();
LocalDate.now(); //w przypadku klas Local* używana jest domyślna strefa czasowa

Kole­jną metodą jest toLo­cal­Date(), toLo­cal­Time(), toIn­stant() i podob­ne (są one dostęp­ne w zaleznoś­ci od konkret­nych implementacji).

Klasa Instant ma dodatkowo metody atZone(ZoneId) oraz atOffset(ZoneOffset), które pozwala­ją zamienić obiekt typu Instant na reprezen­tację konkret­nej daty i godziny w określonej stre­fie czasowej.

Wszys­tkie metody pod­sumowu­je poniższa tabelka:

Meto­da Instant Local­Time Local­Date­Time Off­set­Date­Time Zoned­Date­Time Typ zwracany Opis
 (staty­cz­na) now()            (różne)  Zwraca instanc­je danego typu opisu­jącą ‘ter­az’
toIn­stant()           Instant  Zwraca obiekt typu Instant reprezen­tu­ją­cy ten sam moment
toLo­cal­Date­Time()           Local­Date­Time  Zwraca obiekt reprezen­tu­ją­cy lokalne datę i czas w tym samym momencie
toLo­cal­Time()           Local­Time  Zwraca obiekt reprezen­tu­ją­cy lokalny czas w tym samym momencie
toLo­cal­Date()           Local­Date  Zwraca obiekt reprezen­tu­ją­cy lokalną datę w tym samym momencie
atZone()           Zoned­Date­Time  Zwraca obiekt reprezen­tu­ją­cy określony moment lub lokalny czas jako ZonedDateTime
atOff­set()           Off­set­Date­Time  Zwraca obiekt reprezen­tu­ją­cy określony moment lub lokalny czas jako OffsetDateTime

Którą reprezentację wybrać

Zami­ast jed­nej klasy wcześniej mamy ter­az kil­ka różnych opcji pozwala­ją­cych reprezen­tować datę i czas — przyjrzyjmy się więc na chwilę, jakie są między nimi różnice i którą imple­men­tację wybrać w jakiej sytuacji.

Instant — ta klasa najlepiej nada­je się do reprezen­towa­nia cza­su w sposób, który będzie przetwarzany przez sys­tem i nie będzie wyświ­et­lany użytkown­ikom koń­cowym. Dobrym przykła­dem są np. sys­te­my, których ele­men­ty komu­niku­ją się ze sobą wewnętrznie albo zapisu­ją infor­ma­c­je służące do audy­tu — ważny jest dokład­ny punkt w cza­sie, a nie jego reprezen­tac­ja w określonej stre­fie czasowej.

Zoned­Date­Time — uży­wamy wszędzie tam, gdzie istot­na jest data i godz­i­na z punk­tu widzenia prak­ty­cznego (czyli np. wyświ­etle­nie jej użytkown­ikowi, porów­nanie z inny­mi, ew kon­wer­s­ja) — dobrym przykła­dem może być aplikac­ja do zarządza­nia kalen­darzem — w razie potrze­by taką datę może­my skon­wer­tować do innej stre­fy cza­sowej. Ta klasa jest najlep­szym wyborem dla ogól­nego przy­pad­ku, choć cza­sem wygod­niejsze może być prze­chowywanie osob­no stre­fy cza­sowej użytkown­i­ka i punk­tu w cza­sie (w formie obiek­tu typu Instant) i łącze­nie obu dopiero wyświetlając.

Off­set­Date­Time — zas­tosowanie ma podob­ne, jak Zoned­Date­Time, ale w prak­tyce uży­wana jest w sytu­acji, w której nie mamy dokład­nej infor­ma­cji o stre­fie cza­sowej pow­iązanej z lokaliza­cją, ale mamy infor­ma­c­je o prze­sunię­ciu cza­sowym (przykła­dem może być np. par­sowanie dat ze Stringów — częs­to taka reprezen­tac­ja zaw­iera stre­fę cza­sową w reprezen­tacji np. +03:00). Z tego powodu najczęś­ciej spotkasz się z tą reprezen­tacją w sys­temach, które np. impor­tu­ją dane w postaci plików.

Local­Date­Time — tą klasę wybierze­my, jeśli sys­tem jest ‘lokalny’, tzn. dzi­ała tylko w określonym budynku, urządze­niu (np. aplikac­ja mobil­na) itp, a infor­ma­c­ja, którą opisu­je­my, nie będzie udostęp­ni­ana poza rzec­zonym sys­te­mem. Real­nie prak­ty­czne zas­tosowa­nia są dość ogranic­zone i częs­to sprowadza­ją się do wyświ­et­la­nia — np. poprzez kon­wer­sję innych typów do Local­Date­Time w celu ich prezen­tacji użytkown­ikowi. Moż­na wyko­rzys­tać do prowadzenia jakiejś formy ‘dzi­en­ni­ka zdarzeń’ przez fizy­czną osobę, ale w tylko nieco bardziej ogól­nym przy­pad­ku lep­szym wyborem będzie już Zoned­Date­Time. Tej klasy uży­je­my także w sytu­acji, kiedy nie mamy infor­ma­cji o stre­fie cza­sowej użytkown­i­ka, a mamy podaną przez niego datę i godzinę.

To oczy­wiś­cie tylko wskazów­ki, z uwa­gi na określone wyma­gania pro­jek­tu może się okazać, że inna imple­men­tac­ja lep­iej speł­nia Two­je potrze­by. Najważniejsze, żeby znać zale­ty i wady każdej z imple­men­tacji aby móc pod­jąć świadomą decyzję.

Mapowanie na odpowiedniki SQL

Więk­szość pro­jek­tów, z jaki­mi się spotkasz, będzie wyko­rzysty­wała rela­cyjne bazy danych do prze­chowywa­nia infor­ma­cji. Poniższa tabel­ka pod­sumowu­je mapowanie pomiędzy typa­mi w SQL a klasa­mi w Date­Time API.

ANSI SQL Java SE 8
DATE Local­Date
TIME Local­Time
TIMESTAMP Local­Date­Time
TIME WITH TIMEZONE Off­set­Time
TIMESTAMP WITH TIMEZONE Off­set­Date­Time

Zwróć uwagę, że stan­dar­d­owe typy są mapowane na Local*. Należy o tym pamię­tać, w prze­ci­wnym razie mapu­jąc je np. na obiek­ty klasy Zoned­Date­Time przyję­ta stre­fa cza­sowa będzie domyśl­ną stre­fą JVM, co może nie odpowiadać rzeczywistości.

Wyświetlanie i parsowanie

W nowym API Javy dodana została klasa Date­Time­For­mat­ter, która funkcjon­al­noś­cią jest zbliżona do Sim­ple­Date­For­mat, która ist­ni­ała wcześniej. Ogólne założe­nie jest proste — inic­jal­izu­je­my for­mater poda­jąc mu wzorzec daty, jakiego oczeku­je­my lub jaki chce­my wyp­isać, po czym przekazu­je­my mu obiekt daty lub wczy­tu­je­my dane do takiego obiektu.

Aby sfor­ma­tować datę do wybranego for­matu, wystar­czy poniższy frag­ment kodu:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
String text = date.toString(formatter);

Z kolei wczy­tanie daty o zadanym for­ma­cie z ciągu znaków do obiek­tu wyglą­da nastepująco:

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
LocalDate date = LocalDate.parse(text, formatter);

Zwróć uwagę, że meto­da parse jest metodą nie samego for­mat­era, ale konkret­nych klas reprezen­tu­ją­cych datę. Dzię­ki temu możesz od razu utworzyć obiekt o intere­su­ją­cym Cię typ­ie (np. Local­Date), bez koniecznoś­ci konwersji.

Dawne API i różnice

Poza dużo więk­szym wyborem jeśli chodzi o możli­we imple­men­tac­je, zmieniła się cała kon­cepc­ja- zobaczmy najważniejsze zmi­any ‘ogólne’.

  • Obiek­ty stały się niezmi­enne (Immutable), oper­ac­je na nich zwraca­ją nową instancję zami­ast mody­fikować bieżącą — ta pozornie nieis­tot­na zmi­ana pozwala tworzyć poprawne obiek­ty trans­fer­owe (które nie powin­ny mieć możli­woś­ci mody­fikacji), ułatwia też utrzy­manie kodu (np. pola z mody­fika­torem final nie zmienią wartoś­ci w trak­cie dzi­ała­nia aplikacji)
  • Obiek­ty pozwala­ją reprezen­tować samą datę lub samą godz­inę, także bez pow­iązanej infor­ma­cji o stre­fie cza­sowej, poza lep­szym odzwier­ciedle­niem rzeczy­wis­toś­ci, pozwala to także dokład­niej mapować typy
  • Czas jest reprezen­towany z nanosekun­dową dokład­noś­cią (wcześniej: milisekun­dową) — raczej nieis­totne dla użytkown­ików koń­cowych, ważniejsze z punk­tu widzenia kole­jkowa­nia zdarzeń następu­ją­cych krótko po sobie, oblicza­nia małych odstępów cza­su itp.
  • Obiekt reprezen­tu­ją­cy punkt w cza­sie odłąc­zony od kon­cepcji kalen­darza, dat i godzin
  • Wprowad­zono poję­cie cza­su jako odstępu pomiędzy dwoma punk­ta­mi w czasie
  • API jest Domain-Dri­ven, tzn. pow­stało poprzez mapowanie rzeczy­wis­toś­ci na klasy
  • Nowe API jest nieza­leżne od kalen­darza — daty mogą być reprezen­towane nie tylko w znanym nam kalen­darzu, ale także np. w Bud­dyjskim lub Japońskim — nie jest to problemem

Zmi­an jest oczy­wiś­cie dużo więcej, te powyższe to tylko te subiek­ty­wnie najważniejsze z punk­tu widzenia praktycznego.

Podsumowanie

API związane z data­mi, które wprowad­zono w Javie 8 zaw­iera sporo nowoś­ci, ale są to zde­cy­dowanie potrzeb­ne i przy­datne zmi­any. Część rozwiązań może wydawać się niein­tu­icyj­na, ale są one prze­myślane i mają sens, a to że dzi­ała­ją tak a nie inaczej najczęś­ciej podyk­towane jest jakim­iś dzi­wny­mi rzeczy­wisty­mi przy­pad­ka­mi, które muszą modelować.

Nie zmienia to fak­tu, że zna­jo­mość tego API i jego poprawne uży­cie to abso­lut­nie pod­stawy, które na dodatek mogą znacznie ułatwić Ci pracę i poz­wolą uniknąć wielu potenc­jal­nych prob­lemów czy pomyłek.

Dodatkowe materiały