Date i Time API w Java 8 – Nowoczesne zarządzanie czasem

TL;DR: Java 8 wprowadza kompletnie nowe API do obsługi dat i czasu, zastępujące problematyczne klasy Date i Calendar. LocalDate, LocalTime i LocalDateTime to immutable klasy które rozwiązują większość problemów ze starymi rozwiązaniami. Nowe API jest thread-safe, intuicyjne i znacznie bardziej czytelne.

Praca z datami i czasem w Javie była zawsze problematyczna. Klasy Date i Calendar miały wiele wad – były mutable, nie były thread-safe i ich API było nieintuicyjne. Java 8 w końcu rozwiązuje te problemy wprowadzając kompletnie nowe Date and Time API oparte na bibliotece Joda-Time.

Dlaczego to ważne

Obsługa dat i czasu to jeden z najczęstszych problemów w aplikacjach biznesowych. Każdy system musi zarządzać terminami, timestampami, strefami czasowymi. Stare API Javy powodowało więcej problemów niż rozwiązywało – błędy związane z mutowalnością obiektów, problemy z wielowątkowością, nieintuicyjne nazewnictwo. Nowe API Java 8 to game-changer który sprawia, że praca z czasem staje się przyjemnością zamiast udręki.

Co się nauczysz:

  • Podstawowe klasy nowego Date and Time API (LocalDate, LocalTime, LocalDateTime)
  • Jak tworzyć, parsować i formatować daty
  • Wykonywanie operacji na datach (dodawanie, odejmowanie, porównywanie)
  • Pracę ze strefami czasowymi używając ZonedDateTime
  • Migrację ze starych klas Date/Calendar do nowego API
  • Najlepsze praktyki i częste pułapki przy pracy z czasem
Wymagania wstępne: Podstawowa znajomość Javy, umiejętność tworzenia obiektów i wywoływania metod. Przydatna będzie znajomość problemów z klasami Date/Calendar, ale nie jest wymagana.

Podstawowe klasy nowego API

Nowe Date and Time API wprowadza kilka kluczowych klas, każda odpowiedzialna za inny aspekt czasu:

LocalDate – reprezentuje datę bez czasu (np. 2017-11-05). Idealna do urodzin, terminów, dat ważności.
LocalTime – reprezentuje czas bez daty (np. 14:30:15). Używana do godzin otwarcia, alarmy, czasomierze.
LocalDateTime – kombinuje datę i czas bez strefy czasowej (np. 2017-11-05T14:30:15). Najbardziej powszechna w aplikacjach.
// Importy dla nowego API
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeBasics {
    public static void main(String[] args) {
        // Tworzenie dat różnymi sposobami
        LocalDate today = LocalDate.now();
        LocalDate specificDate = LocalDate.of(2017, 11, 5);
        LocalDate fromString = LocalDate.parse("2017-11-05");
        
        System.out.println("Dziś: " + today);
        System.out.println("Konkretna data: " + specificDate);
        System.out.println("Z tekstu: " + fromString);
        
        // Tworzenie czasu
        LocalTime now = LocalTime.now();
        LocalTime specificTime = LocalTime.of(14, 30, 15);
        
        System.out.println("Teraz: " + now);
        System.out.println("14:30:15: " + specificTime);
        
        // Kombinacja daty i czasu
        LocalDateTime dateTime = LocalDateTime.now();
        LocalDateTime specific = LocalDateTime.of(2017, 11, 5, 14, 30, 15);
        
        System.out.println("Data i czas: " + dateTime);
        System.out.println("Konkretny moment: " + specific);
    }
}
Wszystkie nowe klasy są immutable – każda operacja zwraca nowy obiekt zamiast modyfikować istniejący. To eliminuje wiele błędów związanych z mutowalnością.

Formatowanie i parsowanie dat

Jedną z najczęstszych operacji jest konwersja między tekstem a datami. Nowe API wprowadza klasę DateTimeFormatter która zastępuje SimpleDateFormat:

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateTimeFormatting {
    public static void main(String[] args) {
        LocalDate date = LocalDate.of(2017, 11, 5);
        LocalDateTime dateTime = LocalDateTime.of(2017, 11, 5, 14, 30, 15);
        
        // Predefiniowane formatery
        System.out.println("ISO format: " + date.toString()); // 2017-11-05
        System.out.println("Basic ISO: " + date.format(DateTimeFormatter.BASIC_ISO_DATE)); // 20171105
        
        // Własne formatery
        DateTimeFormatter polishFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy");
        DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss");
        DateTimeFormatter fullFormat = DateTimeFormatter.ofPattern("dd MMMM yyyy, HH:mm");
        
        System.out.println("Polski format: " + date.format(polishFormat)); // 05.11.2017
        System.out.println("Tylko czas: " + dateTime.format(timeFormat)); // 14:30:15
        System.out.println("Pełny format: " + dateTime.format(fullFormat)); // 05 November 2017, 14:30
        
        // Parsowanie z tekstu
        LocalDate parsedDate = LocalDate.parse("05.11.2017", polishFormat);
        System.out.println("Sparsowana data: " + parsedDate);
    }
}
Pro tip: DateTimeFormatter jest thread-safe w przeciwieństwie do SimpleDateFormat. Możesz bezpiecznie tworzyć statyczne instancje i używać ich w aplikacjach wielowątkowych.

Operacje na datach

Nowe API sprawia, że operacje na datach stają się intuicyjne i czytelne:

import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;

public class DateOperations {
    public static void main(String[] args) {
        LocalDate startDate = LocalDate.of(2017, 11, 5);
        
        // Dodawanie i odejmowanie
        LocalDate nextWeek = startDate.plusWeeks(1);
        LocalDate lastMonth = startDate.minusMonths(1);
        LocalDate in30Days = startDate.plusDays(30);
        
        System.out.println("Start: " + startDate);
        System.out.println("Za tydzień: " + nextWeek);
        System.out.println("Miesiąc temu: " + lastMonth);
        System.out.println("Za 30 dni: " + in30Days);
        
        // Porównywanie dat
        LocalDate date1 = LocalDate.of(2017, 11, 5);
        LocalDate date2 = LocalDate.of(2017, 12, 1);
        
        System.out.println("Date1 przed Date2: " + date1.isBefore(date2)); // true
        System.out.println("Date1 po Date2: " + date1.isAfter(date2)); // false
        System.out.println("Daty równe: " + date1.isEqual(date2)); // false
        
        // Obliczanie różnic
        Period period = Period.between(date1, date2);
        long daysBetween = ChronoUnit.DAYS.between(date1, date2);
        
        System.out.println("Różnica: " + period.getDays() + " dni");
        System.out.println("Dni między: " + daysBetween);
    }
}

Strefy czasowe z ZonedDateTime

Gdy potrzebujesz pracować ze strefami czasowymi, używasz ZonedDateTime:

import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;

public class TimeZoneExample {
    public static void main(String[] args) {
        // Aktualna strefa czasowa
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println("Teraz: " + now);
        
        // Konkretna strefa czasowa
        ZoneId warsawZone = ZoneId.of("Europe/Warsaw");
        ZoneId newYorkZone = ZoneId.of("America/New_York");
        
        LocalDateTime localTime = LocalDateTime.of(2017, 11, 5, 14, 30);
        
        ZonedDateTime warsawTime = localTime.atZone(warsawZone);
        ZonedDateTime newYorkTime = warsawTime.withZoneSameInstant(newYorkZone);
        
        System.out.println("Warszawa: " + warsawTime);
        System.out.println("Nowy Jork: " + newYorkTime);
        
        // Lista dostępnych stref
        ZoneId.getAvailableZoneIds()
              .stream()
              .filter(zone -> zone.contains("Europe"))
              .sorted()
              .forEach(System.out::println);
    }
}
Uwaga: Zawsze używaj pełnych nazw stref czasowych jak „Europe/Warsaw” zamiast skrótów jak „CET”. Skróty mogą być niejednoznaczne i prowadzić do błędów.

Migracja ze starych klas

Jeśli masz istniejący kod używający Date i Calendar, możesz stopniowo migrować:

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Calendar;

public class MigrationExample {
    public static void main(String[] args) {
        // Konwersja Date -> LocalDateTime
        Date oldDate = new Date();
        LocalDateTime newDateTime = oldDate.toInstant()
                                          .atZone(ZoneId.systemDefault())
                                          .toLocalDateTime();
        
        System.out.println("Stara Date: " + oldDate);
        System.out.println("Nowa LocalDateTime: " + newDateTime);
        
        // Konwersja LocalDateTime -> Date (gdy API wymaga Date)
        LocalDateTime localDateTime = LocalDateTime.now();
        Date convertedDate = Date.from(localDateTime.atZone(ZoneId.systemDefault())
                                                  .toInstant());
        
        System.out.println("LocalDateTime: " + localDateTime);
        System.out.println("Skonwertowana Date: " + convertedDate);
        
        // Zastępowanie Calendar
        Calendar calendar = Calendar.getInstance();
        calendar.set(2017, Calendar.NOVEMBER, 5); // Uwaga: miesiące od 0!
        
        // Nowy sposób - znacznie czytelniejszy
        LocalDateTime equivalent = LocalDateTime.of(2017, 11, 5, 0, 0);
        
        System.out.println("Calendar date: " + calendar.getTime());
        System.out.println("LocalDateTime: " + equivalent);
    }
}
Pułapka: W Calendar miesiące liczą się od 0 (styczeń = 0), ale w nowym API miesiące liczą się normalnie od 1. To częsta przyczyna błędów podczas migracji!

Najlepsze praktyki

Używaj odpowiedniej klasy:

  • LocalDate – dla dat urodzenia, terminów, dat ważności
  • LocalTime – dla godzin otwarcia, alarmów
  • LocalDateTime – dla timestampów w aplikacji
  • ZonedDateTime – gdy potrzebujesz stref czasowych
  • Instant – dla timestampów UTC w bazach danych
// Przykłady dobrych praktyk
public class BestPractices {
    // Statyczne formatery - thread-safe i wielokrotnego użytku
    private static final DateTimeFormatter POLISH_DATE = 
        DateTimeFormatter.ofPattern("dd.MM.yyyy");
    
    public static void demonstrateBestPractices() {
        // 1. Zawsze używaj LocalDate dla samych dat
        LocalDate birthDate = LocalDate.of(1990, 5, 15);
        LocalDate today = LocalDate.now();
        
        // 2. Obliczanie wieku
        Period age = Period.between(birthDate, today);
        System.out.println("Wiek: " + age.getYears() + " lat");
        
        // 3. Walidacja dat
        if (birthDate.isAfter(today)) {
            throw new IllegalArgumentException("Data urodzenia nie może być w przyszłości");
        }
        
        // 4. Formatowanie z użyciem stałych
        String formattedDate = birthDate.format(POLISH_DATE);
        System.out.println("Sformatowana data: " + formattedDate);
        
        // 5. Parsowanie z obsługą błędów
        try {
            LocalDate parsed = LocalDate.parse("invalid-date");
        } catch (Exception e) {
            System.out.println("Błędny format daty: " + e.getMessage());
        }
    }
}
Typowy błąd początkujących: Próba modyfikacji obiektów LocalDate/LocalDateTime. Pamiętaj – te klasy są immutable! Każda operacja zwraca nowy obiekt.
Czy powinienem używać Date i Calendar w nowych projektach?

Nie! W Java 8+ zawsze używaj nowego Date and Time API. Stare klasy mają zbyt wiele problemów i są deprecated w praktyce. Używaj ich tylko gdy zmuszają Cię legacy biblioteki.

Jak radzić sobie z bazami danych?

Większość nowoczesnych sterowników JDBC wspiera nowe typy automatycznie. W Hibernate możesz mapować LocalDate na DATE, LocalDateTime na TIMESTAMP. Dla UTC używaj Instant.

Czy DateTimeFormatter jest thread-safe?

Tak! W przeciwieństwie do SimpleDateFormat, DateTimeFormatter można bezpiecznie używać w środowiskach wielowątkowych. Twórz statyczne instancje i używaj wielokrotnie.

Jak porównywać daty z różnych stref czasowych?

Konwertuj obie daty do tej samej strefy czasowej lub używaj Instant.toEpochMilli() aby porównać timestampy UTC. ZonedDateTime automatycznie uwzględnia strefy w porównaniach.

Co z wydajnością nowego API?

Nowe API jest równie szybkie co stare, a w wielu przypadkach szybsze dzięki lepszym optymalizacjom. Immutability paradoksalnie poprawia wydajność dzięki lepszemu cache’owaniu przez JVM.

Jak testować kod z datami?

Używaj Clock.fixed() w testach aby „zatrzymać” czas na konkretnej wartości. Możesz też wstrzykiwać Clock jako dependency i mockować go w testach jednostkowych.

Czy mogę mieszać stare i nowe API?

Tak, ale ostrożnie. Są metody konwersji między nimi, ale lepiej systematycznie migrować cały kod. Mieszanie może prowadzić do subtelnych błędów, szczególnie ze strefami czasowymi.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Stwórz kalkulator wieku: Napisz aplikację która przyjmuje datę urodzenia i oblicza dokładny wiek w latach, miesiącach i dniach. Dodaj walidację (data nie może być w przyszłości) i formatowanie wyników. Bonus: dodaj obliczanie ile dni zostało do następnych urodzin!

Czy masz już doświadczenie z nowym Date and Time API? A może nadal używasz starych klas Date i Calendar? Podziel się swoimi doświadczeniami w komentarzach!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry