Praca z czasem i datami w języku Java

Czasy zamierzchłe

Przed ósmym wydaniem Javy do obsługi dat używano dwóch klas – Date oraz Calendar. Praca z nimi nie należała do najprzyjemniejszych. Na co dzień irytowała niespójność API (np. dni numerowane od zera, a miesiące od jedynki), a przy szczególnych okazjach brak zabezpieczeń, gdy kilka wątków pracowało na tym samym obiekcie. Efekt był taki, że popularność zdobywały alternatywne biblioteki do zarządzania czasem, najpopularniejszą z nich była ta o nazwie Joda-Time

Nie poświęcimy tym dwóm klasom tutaj miejsca, bo Java 8 przyniosła ze sobą coś lepszego.

Java 8

W Javie o numerze 8, przy współpracy Oracle i autora wspomnianego wyżej Joda-Time powstał nowy standard pracy z datami i czasem.

Po pierwsze klasy są niezmienne (immutable), co przekłada się na bezpieczeństwo pracy wielowątkowej. Teraz zmiana obiektu daty (godziny, dnia) nie zmienia obiektu jako takiego, tylko zwraca zupełnie nowy.

Po drugie podzielono klasę Date na klasy – LocalDate i LocalTime. Pierwsza z nich odpowiada za datę, a druga za godzinę. Jest również LocalDateTime, która zawiera komplet informacji.

Podstawowe operacje

Wszystkie te trzy klasy zawierają spójne metody fabrykujące:

LocalDate today = LocalDate.now();
LocalDate whenCreated = LocalDate.of(2021, Month.FEBRUARY, 22);
LocalDate dateFromString = LocalDate.parse("2021-02-22");
System.out.println(whenCreated); // 2021-02-22

LocalTime now = LocalTime.now();
LocalTime whenWritten = LocalTime.of(22, 12);
LocalTime timeFromString = LocalTime.parse("22:12:23");
System.out.println(whenWritten); // 22:12

.now zwraca obecną datę/godzinę/komplet danych. .of zwraca moment w czasie na podstawie podanych parametrów. .parse buduje ze Stringa. Dodatkowo domyślnie wszystkie te klasy posiadają domyślną, przyzwoitą implementację .toString, więc jeśli nie zależy nam na żadnym konkretnym formacie, to możemy bazować na domyślnym, bez potrzeby użycia formattera.

Za pomocą standardowych getterów możemy pobrać poszczególne elementy godziny, bądź daty.

now.getSecond(); // 38
now.getMinute(); // 34
today.getMonth(); // FEBUARY (obiekt na bazie enuma Month)

Modyfikowanie czasu i dat

Do modyfikowania czasu i daty nie używamy setterów. Nowe API składa się z klas niemodyfikowalnych. Do zmian wybranej daty używa się metody .with, która zawsze zwraca nowy obiekt.

        LocalTime inAnHour = now.withHour(23);
        LocalDate inOneYear = today.withYear(2022);

Za pomocą metody .at możemy do daty dodać godzinę (bądź odwrotnie) i otrzymać obiekt typu LocalDateTime.

LocalDateTime inOneYearHourLater = inOneYear.atTime(inAnHour);

Dodatkowo nowe API posiada zestaw bytów o nazwie adjuster. Jest to zestaw metod, który opakowuje najpopularniejsze operacje na datach związane z poszukiwaniem np. najbliższego poniedziałku od dziś, albo jaki będzie ostatni dzień miesiąca.

LocalDate nextMonday = today.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
LocalDate lastDay = today.with(TemporalAdjusters.lastDayOfMonth());

Wszystkie predefiniowane “nastawiacze” dostępne są w klasie TemporalAdjusters jako metody statyczne.

Timezone

Strefy czasowe, czyli utrapienie każdego przyzwoitego programisty.

Nowe API oferuję klasę ZoneId do identyfikacji strefy czasowej, ma ich wbudowane 601 (na czas Javy 15). Dzięki tej klasie mamy dostęp do tych wszystkich stref czasowych.

ZoneId myZone = ZoneId.systemDefault();
System.out.println(myZone); // Europe/Warsawc
System.out.println(ZoneId.getAvailableZoneIds().size());
ZoneId.getAvailableZoneIds().forEach( zone -> System.out.println(zone));

Pracując ze strefami czasowymi, to używamy klasy ZonedDateTime.

ZonedDateTime nowInCracow = ZonedDateTime.now();
System.out.println(nowInCracow);  // 2021-02-22T23:26:19.987411700+01:00[Europe/Warsaw]

ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
System.out.println(nowInTokyo); // 2021-02-23T07:29:08.426427400+09:00[Asia/Tokyo]

Jeśli chcemy zapisać daty do bazy danych w tym formacie, mówiącym nam wszystko, możemy użyć klasy OffsetDateTime, która zostawia informacje o różnicy czasu, natomiast ucina tekst z ZoneId.

ZonedDateTime nowInCracow = ZonedDateTime.now();
System.out.println(nowInCracow.toOffsetDateTime()); // 2021-02-22T23:37:06.983230700+01:00

ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(nowInTokyo.toOffsetDateTime()); // 2021-02-23T07:37:06.983230700+09:00

Period i Duration

Ostatnimi wartymi wspomnienia klasami w nowym Date-Time API są Period i Duration.

Pierwsza nich określa okres czasu mierzony w latach, miesiącach i dniach, czyli w odniesieniu do daty.

Dzięki niemu możemy dowiedzieć się jaka data będzie za jeden rok, dwa miesiące i trzy dni, albo sprawdzić ile dni zostało do piątku.

LocalDate now = LocalDate.now();
LocalDate plus = now.plus(Period.of(1, 2, 3));
System.out.println(plus); //2022-04-25

Period between = Period.between(LocalDate.now(), LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.FRIDAY)));
System.out.println(between.getDays()); // 4

Duration natomiast jest tym samym natomiast w odniesieniu sekund i nanosekund. Gdybyśmy chcieli sprawdzić ile nanosekund zajmuje Javie wypisanie tekstu na konsolę;

 LocalTime start = LocalTime.now();

 System.out.println("Clockwork Java");

 LocalTime end = LocalTime.now();

 Duration between = Duration.between(start, end);
        
 System.out.println(between.getNano());

Choć do tego celu zamiast LocalTime lepiej jest użyć po prostu System.currentTimeMilis();

Konwersja z Date na LocalDateTime i vice-versa

Czasami nie mamy wyboru i pracując ze starym kodem musimy pracować na typie Date. Nie zawsze mamy możliwość zmiany bebechów systemu. Wówczas, zamiast męczyć się z pracą z Date proponuję konwersje na LocalDate/Time, wykonanie stosownej logiki, a potem powrót.

 Date oldStyle = new Date();

 // Date -> LocalDateTime
 LocalDateTime localDateTime = LocalDateTime.ofInstant(oldStyle.toInstant(), ZoneId.systemDefault());

 // LocalDateTime -> Date
 Date from = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());

To by było na tyle jeśli chodzi o podstawy pracy z LocalDate, LocalTime i LocalDateTime.

By być na bieżąco i mieć realny wpływ na tematykę tworzonych artykułów zapraszam do dołączenia do mojego newslettera.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *