Date Time API
The java.time API (JSR-310), introduced in Java 8, is a complete replacement for the legacy java.util.Date and java.util.Calendar classes, built around immutable, thread-safe value types that clearly separate human-readable dates and times from machine instants, durations, and time zone reasoning. The legacy API was widely regarded as one of Java's worst-designed parts: Date was mutable, not thread-safe, used confusing zero-based months and an offset-from-1900 year representation, and Calendar's API was verbose and error-prone, while neither class clearly distinguished a date from a timestamp from a duration. This entry covers the motivation and design principles behind java.time, the core classes (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period), formatting and parsing with DateTimeFormatter, time zone handling, and interoperability with the legacy API.
Motivation — Why java.util.Date and Calendar Were Replaced
// ── Legacy API problems ─────────────────────────────────────────────────
Date d = new Date(2024 - 1900, 0, 15); // confusing: year offset, zero-based month
d.setMonth(d.getMonth() + 1); // MUTATES the existing instance — unsafe to share
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); // NOT thread-safe
// Sharing `sdf` across threads can corrupt parse/format results unpredictably
// ── java.time equivalents — immutable, clear semantics ─────────────────
LocalDate ld = LocalDate.of(2024, Month.JANUARY, 15); // explicit month enum, real year
LocalDate next = ld.plusMonths(1); // returns a NEW instance
System.out.println(ld); // 2024-01-15 — original untouched
System.out.println(next); // 2024-02-15
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // thread-safe, immutable
String formatted = ld.format(fmt);
LocalDate parsed = LocalDate.parse("2024-01-15", fmt);Core Types — Local Dates/Times, Instant, Duration, and Period
// ── LocalDate / LocalTime / LocalDateTime — no zone information ────────
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dt = LocalDateTime.of(today, now);
LocalDateTime birthday = LocalDateTime.of(1990, Month.MARCH, 12, 0, 0);
// ── ZonedDateTime — unambiguous real-world moment ───────────────────────
ZonedDateTime meetingInTokyo = ZonedDateTime.of(2024, 6, 19, 9, 0, 0, 0, ZoneId.of("Asia/Tokyo"));
ZonedDateTime sameMomentInLA = meetingInTokyo.withZoneSameInstant(ZoneId.of("America/Los_Angeles"));
System.out.println(meetingInTokyo); // 2024-06-19T09:00+09:00[Asia/Tokyo]
System.out.println(sameMomentInLA); // 2024-06-18T17:00-07:00[America/Los_Angeles]
// ── Instant — point on the UTC timeline, for timestamps/logging ────────
Instant start = Instant.now();
// ... do some work ...
Instant end = Instant.now();
long epochMillis = end.toEpochMilli();
// ── Duration — exact elapsed time, seconds-based ────────────────────────
Duration elapsed = Duration.between(start, end);
System.out.println(elapsed.toMillis() + " ms elapsed");
Duration timeout = Duration.ofSeconds(30);
LocalTime later = LocalTime.now().plus(timeout);
// ── Period — calendar-based elapsed time, varies in length ─────────────
LocalDate birth = LocalDate.of(1990, 3, 12);
Period age = Period.between(birth, LocalDate.now());
System.out.println(age.getYears() + " years, " + age.getMonths() + " months");
// Why the distinction matters:
LocalDateTime jan31 = LocalDateTime.of(2024, 1, 31, 0, 0);
LocalDateTime plusPeriodMonth = jan31.plus(Period.ofMonths(1)); // 2024-02-29 (leap year, clamps to month-end)
// A naive "add 30 days as Duration" would NOT land on the same calendar-meaningful dateFormatting, Parsing, Time Zones, and Legacy Interop
// ── Formatting and parsing ──────────────────────────────────────────────
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm");
LocalDateTime dt = LocalDateTime.of(2024, 6, 19, 14, 30);
System.out.println(dt.format(custom)); // 19 Jun 2024, 14:30
LocalDate iso = LocalDate.parse("2024-06-19"); // uses ISO_LOCAL_DATE by default
DateTimeFormatter localeFmt = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.FRENCH);
System.out.println(dt.format(localeFmt)); // 19 juin 2024
// ── Time zones — ZoneId vs ZoneOffset ───────────────────────────────────
ZoneId tokyo = ZoneId.of("Asia/Tokyo"); // tracks DST rules historically/automatically
ZoneOffset fixed = ZoneOffset.of("+09:00"); // fixed offset, no DST awareness
ZonedDateTime z1 = ZonedDateTime.now(tokyo);
System.out.println(ZoneId.systemDefault()); // JVM's configured default zone
// Best practice: store/transmit as Instant (UTC), convert to zone only for display:
Instant storedTimestamp = Instant.now();
ZonedDateTime displayed = storedTimestamp.atZone(ZoneId.of("America/New_York"));
// ── Legacy interop — explicit conversions only ──────────────────────────
Date legacyDate = Date.from(Instant.now()); // java.time -> legacy
Instant fromLegacy = legacyDate.toInstant(); // legacy -> java.time
Calendar cal = GregorianCalendar.from(ZonedDateTime.now());
ZonedDateTime fromCal = ((GregorianCalendar) cal).toZonedDateTime();