1. Kontekst historyczny: jak pracowano z datami przed Javą 8
W zamierzchłych czasach (przed Javą 8) do pracy z datami i czasem używano klas java.util.Date, java.util.Calendar oraz do formatowania — java.text.SimpleDateFormat. To był ten przypadek, gdy programiści na całym świecie wzdychali i, zgrzytając zębami, pisali coś w rodzaju:
import java.util.Date;
Date now = new Date();
System.out.println(now); // Wypisze coś dziwnego w rodzaju "Wed Jun 05 14:15:22 MSK 2025"
Brzmi prosto, ale w praktyce nie było tak różowo. Oto tylko niektóre „uroki” starego API:
- Date — obiekt mutowalny (mutable). Można go było przypadkowo zmienić, co często prowadziło do błędów.
- Miesiące w Date i Calendar zaczynały się od zera (styczeń — to 0, grudzień — 11), a dni — od jedynki.
- SimpleDateFormat nie był bezpieczny wątkowo: jeśli dwa wątki formatowały datę jednocześnie, można było otrzymać nieoczekiwane wyniki.
- Mnóstwo metod oznaczono jako @Deprecated (przestarzałe), a IDE nieustannie straszyło żółtymi ostrzeżeniami.
- Praca ze strefami czasowymi była prawdziwą udręką: łatwo było pomylić czas lokalny z UTC, a o zmianie czasu lepiej w ogóle nie wspominać.
Przykład bólu
import java.util.Date;
Date date = new Date(2025, 5, 1); // rok 2025, miesiąc 5 (czerwiec?), dzień 1
System.out.println(date); // Nie to, czego się spodziewasz!
2. Pojawienie się java.time: nowe podejście
Do 2014 roku stało się jasne: stare API nie było tylko niewygodne — było niebezpieczne. Dlatego w Javie pojawił się nowy pakiet — java.time, implementujący specyfikację JSR‑310. To API było inspirowane popularną biblioteką Joda-Time i szybko stało się standardem de facto.
Najważniejsze pakiety i klasy
- java.time — podstawowy pakiet, w którym znajdują się wszystkie nowe klasy do pracy z datą i czasem.
- java.time.format — do formatowania i parsowania daty i czasu.
- java.time.temporal — do bardziej zaawansowanych operacji czasowych.
- java.time.zone — do pracy ze strefami czasowymi.
Oto główni bohaterowie nowego API:
| Klasa | Do czego? | Przykład użycia |
|---|---|---|
|
Tylko data (rok, miesiąc, dzień) | Urodziny, bez czasu |
|
Tylko czas (godziny, minuty, sekundy) | Czas spotkania, bez daty |
|
Data i czas, bez strefy czasowej | Zdarzenie lokalne |
|
Data i czas ze strefą czasową | Spotkanie w Mińsku wg czasu lokalnego |
|
Bezwzględny punkt czasu (UTC) | Znacznik zdarzenia w logu |
|
Odstęp czasu (godz., min., sek.) | Czas trwania połączenia |
|
Okres (lata, miesiące, dni) | Staż pracy, wiek |
Przykład: tworzenie daty po nowemu
import java.time.LocalDate;
LocalDate today = LocalDate.now();
System.out.println(today); // Na przykład "2025-06-05"
3. Zalety nowego API
Niemutowalność (immutable)
Wszystkie klasy z java.time są niemutowalne. To znaczy: jeśli utworzysz obiekt LocalDate, nie możesz go zmienić. Każda operacja (na przykład dodanie dnia) zwraca nowy obiekt.
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
System.out.println(today); // 2025-06-05
System.out.println(tomorrow); // 2025-06-06
Jawna praca ze strefami czasowymi
W starym API łatwo było zapomnieć, w jakiej strefie czasowej znajduje się data. W java.time wszystko jest jawne: jeśli potrzebna jest strefa czasowa — użyj ZonedDateTime, jeśli nie — użyj LocalDateTime.
import java.time.ZonedDateTime;
import java.time.ZoneId;
ZonedDateTime MinskTime = ZonedDateTime.now(ZoneId.of("Europe/Minsk"));
System.out.println(MinskTime); // 2025-06-05T14:23:45+03:00[Europe/Minsk]
Wygodne metody do obliczeń i porównywania
LocalDate today = LocalDate.now();
LocalDate nextMonth = today.plusMonths(1);
boolean isAfter = LocalDate.now().plusDays(1).isAfter(today); // true
Formatowanie i parsowanie
Formatowanie i parsowanie — przez DateTimeFormatter (szczegóły — na następnej lekcji):
import java.time.format.DateTimeFormatter;
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
String formatted = today.format(formatter); // "05.08.2025"
4. Zgodność: jak żyć ze starym kodem
W realnym świecie bardzo często trzeba pracować z bibliotekami lub starszymi systemami, w których używane są Date i Calendar. Na szczęście nowe API jest przyjazne dla dziedzictwa: można konwertować stare typy na nowe i odwrotnie.
Konwersja Date ↔ Instant
import java.util.Date;
import java.time.Instant;
// Date → Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
// Instant → Date
Date dateBack = Date.from(instant);
Konwersja Calendar ↔ ZonedDateTime
import java.util.Calendar;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.util.Date;
// Calendar → ZonedDateTime
Calendar calendar = Calendar.getInstance();
ZonedDateTime zdt = ZonedDateTime.ofInstant(
calendar.toInstant(),
calendar.getTimeZone().toZoneId()
);
// ZonedDateTime → Calendar
Calendar calBack = Calendar.getInstance();
calBack.setTime(Date.from(zdt.toInstant()));
Tabela: mapowanie starych i nowych klas
| Stara klasa | Nowa klasa | Komentarz |
|---|---|---|
|
|
Czas absolutny |
|
|
Data i czas ze strefą czasową |
|
|
Formatowanie/parsowanie dat |
5. Praktyka: pierwsze kroki z java.time
Załóżmy, że masz użytkownika z datą urodzenia. Zapiszmy i wypiszmy tę datę:
import java.time.LocalDate;
public class UserProfile {
private String name;
private LocalDate birthDate;
public UserProfile(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public void printProfile() {
System.out.println("Imię: " + name);
System.out.println("Data urodzenia: " + birthDate);
}
}
public class Main {
public static void main(String[] args) {
UserProfile user = new UserProfile("Alisa", LocalDate.of(1998, 12, 25));
user.printProfile();
}
}
Wynik:
Imię: Alisa
Data urodzenia: 1998-12-25
6. Porównanie: stare API vs nowe API
Przykład: dodanie tygodnia do daty urodzenia
Stary sposób (Date/Calendar):
import java.util.Calendar;
Calendar cal = Calendar.getInstance();
cal.set(1998, Calendar.DECEMBER, 25);
cal.add(Calendar.WEEK_OF_YEAR, 1);
System.out.println(cal.getTime()); // Żmudne i nieoczywiste
Nowy sposób (java.time):
import java.time.LocalDate;
LocalDate birthDate = LocalDate.of(1998, 12, 25);
LocalDate nextWeek = birthDate.plusWeeks(1);
System.out.println(nextWeek); // 1999-01-01
W nowym API kod jest krótszy, prostszy i bezpieczniejszy.
7. Typowe błędy podczas pracy z java.time
Błąd nr 1: zapomniano, że obiekty są niemutowalne.
Jeśli wywołasz date.plusDays(1); i nie zapiszesz wyniku, pierwotna data pozostanie bez zmian.
Błąd nr 2: pomylenie LocalDate i LocalDateTime.
LocalDate przechowuje tylko datę (rok, miesiąc, dzień), a LocalDateTime — również czas. Nie pomyl, jeśli trzeba obsłużyć godziny i minuty.
Błąd nr 3: używanie starych klas w nowych projektach.
Jeśli to możliwe — zawsze używaj java.time. Stare klasy są potrzebne tylko dla zgodności.
Błąd nr 4: niepoprawna praca ze strefami czasowymi.
Jeśli trzeba przechowywać zdarzenie istotne dla różnych regionów, użyj ZonedDateTime lub przynajmniej Instant. LocalDateTime nie zawiera informacji o strefie czasowej!
Błąd nr 5: próba bezpośredniego porównywania LocalDate i LocalDateTime.
To różne typy danych — nie można ich porównywać bezpośrednio. Najpierw sprowadź do jednego typu.
GO TO FULL VERSION