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
Podstawowe klasy nowego API
Nowe Date and Time API wprowadza kilka kluczowych klas, każda odpowiedzialna za inny aspekt czasu:
// 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); } }
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); } }
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); } }
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); } }
Najlepsze praktyki
- 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()); } } }
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.
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.
Tak! W przeciwieństwie do SimpleDateFormat, DateTimeFormatter można bezpiecznie używać w środowiskach wielowątkowych. Twórz statyczne instancje i używaj wielokrotnie.
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.
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.
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.
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:
- Oficjalna dokumentacja Java Time API
- Oracle: Date Time API Tutorial
- Joda-Time – inspiracja dla Java 8 API
🚀 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!