Date and Time API (JSR-310): LocalDate, ZonedDateTime, Duration, Period
Why java.util.Date Had to Go
java.util.Date was introduced in Java 1.0 and has been a source of bugs ever since:
- Mutable —
Datecan be modified after creation, causing subtle bugs when passed between methods - Not thread-safe — mutable state means shared dates need external synchronisation
- Poor API design — year is offset from 1900, months are 0-indexed, no time zone support on
Dateitself - Deprecated methods — most of the useful methods on
Datewere deprecated in Java 1.1 in favour ofCalendar, which is also mutable and complex
// Java 7: classic Date bugs
Date d = new Date(2024 - 1900, 0, 15); // year 2024, January, day 15 — confusing!
d.setMonth(11); // mutates in place — dangerous if shared
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// SimpleDateFormat is NOT thread-safe — can't be shared across threads
JSR-310 (led by Stephen Colebourne, author of Joda-Time) introduced java.time in Java 8: immutable, thread-safe, ISO-8601-first, and far more intuitive.
The java.time Type Hierarchy
java.time
├── LocalDate — date without time or zone (2024-01-15)
├── LocalTime — time without date or zone (14:30:00)
├── LocalDateTime — date + time, no zone (2024-01-15T14:30:00)
├── ZonedDateTime — date + time + zone (2024-01-15T14:30:00+00:00[Europe/London])
├── OffsetDateTime — date + time + UTC offset (2024-01-15T14:30:00+05:30)
├── OffsetTime — time + UTC offset (14:30:00+05:30)
├── Instant — machine timestamp (epoch seconds + nanoseconds)
├── Duration — time-based amount (3 hours 30 minutes)
├── Period — date-based amount (2 years 3 months)
├── ZoneId — time zone identifier (Europe/London, America/New_York)
├── ZoneOffset — fixed UTC offset (+05:30)
└── DateTimeFormatter — parse and format dates
LocalDate
Represents a date without a time or time zone. Use for birthdays, deadlines, working days.
// Creating
LocalDate today = LocalDate.now();
LocalDate specific = LocalDate.of(2024, 1, 15);
LocalDate parsed = LocalDate.parse("2024-01-15"); // ISO-8601 default
LocalDate custom = LocalDate.parse("15/01/2024",
DateTimeFormatter.ofPattern("dd/MM/yyyy"));
// Accessing fields
int year = today.getYear(); // 2026
int month = today.getMonthValue(); // 5 (1-indexed)
Month m = today.getMonth(); // Month.MAY
int day = today.getDayOfMonth(); // 4
DayOfWeek dow = today.getDayOfWeek(); // DayOfWeek.MONDAY
// Manipulating (returns new instance — immutable)
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate nextYear = today.plusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1);
// Comparing
boolean isBefore = LocalDate.of(2023, 1, 1).isBefore(today);
boolean isAfter = today.isAfter(LocalDate.of(2020, 1, 1));
boolean isLeap = today.isLeapYear();
// Calculating gap
long daysBetween = ChronoUnit.DAYS.between(LocalDate.of(2024, 1, 1), today);
LocalTime
Represents a time without a date or time zone. Use for opening hours, schedules.
LocalTime now = LocalTime.now();
LocalTime meeting = LocalTime.of(14, 30); // 14:30:00
LocalTime precise = LocalTime.of(14, 30, 15, 500); // 14:30:15.0000005
LocalTime parsed = LocalTime.parse("14:30:00");
// Manipulating
LocalTime later = meeting.plusHours(2).plusMinutes(15); // 16:45
// Comparing
boolean isBefore = meeting.isBefore(LocalTime.of(17, 0));
// Check if within working hours
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 0);
boolean inWorkHours = !now.isBefore(start) && now.isBefore(end);
LocalDateTime
Combines a date and time. Use for event timestamps where the time zone is determined by context (e.g., the user’s local time).
LocalDateTime now = LocalDateTime.now();
LocalDateTime specific = LocalDateTime.of(2024, 6, 15, 14, 30);
LocalDateTime parsed = LocalDateTime.parse("2024-06-15T14:30:00");
// From LocalDate + LocalTime
LocalDateTime dt = LocalDate.of(2024, 6, 15).atTime(LocalTime.of(14, 30));
// Or
LocalDateTime dt2 = LocalDate.of(2024, 6, 15).atTime(14, 30);
// Extract parts
LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();
// Manipulating
LocalDateTime nextWeek = now.plusWeeks(1);
LocalDateTime truncated = now.truncatedTo(ChronoUnit.HOURS); // zeroes minutes/seconds/nanos
ZonedDateTime
A LocalDateTime combined with a ZoneId. Use when you need to store or transmit a timestamp that is unambiguous across time zones.
ZoneId london = ZoneId.of("Europe/London");
ZoneId newYork = ZoneId.of("America/New_York");
ZoneId mumbai = ZoneId.of("Asia/Kolkata");
// Create
ZonedDateTime nowLondon = ZonedDateTime.now(london);
ZonedDateTime specific = ZonedDateTime.of(
LocalDateTime.of(2024, 6, 15, 14, 30), london);
// Convert to another zone — same instant, different wall clock
ZonedDateTime sameInstantNY = nowLondon.withZoneSameInstant(newYork);
// Format
String formatted = nowLondon.format(DateTimeFormatter.RFC_1123_DATE_TIME);
// List all available zone IDs
ZoneId.getAvailableZoneIds().stream()
.filter(id -> id.startsWith("America/"))
.sorted()
.forEach(System.out::println);
ZonedDateTime vs OffsetDateTime
ZonedDateTimeuses a named zone (Europe/London) which understands DST transitionsOffsetDateTimeuses a fixed offset (+00:00) which does not adjust for DST
Use ZonedDateTime when zone rules (DST) matter. Use OffsetDateTime for serialisation or when storing a historical timestamp where the exact offset matters more than the zone name.
Instant
Represents a moment on the timeline as epoch seconds + nanoseconds. This is the machine-time representation — no time zone concept.
Instant now = Instant.now(); // current UTC timestamp
Instant epoch = Instant.EPOCH; // 1970-01-01T00:00:00Z
// From epoch millis (for legacy interop)
Instant fromMillis = Instant.ofEpochMilli(System.currentTimeMillis());
// Arithmetic
Instant later = now.plusSeconds(3600);
Instant earlier = now.minus(Duration.ofMinutes(30));
// Compare
boolean isBefore = now.isBefore(later);
// Convert to ZonedDateTime for display
ZonedDateTime displayTime = now.atZone(ZoneId.of("Europe/London"));
// Convert legacy Date to Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
// Convert Instant to legacy Date
Date back = Date.from(instant);
Use Instant for: log timestamps, audit trails, inter-system communication, anything that needs to be timezone-agnostic.
Duration
A time-based amount — hours, minutes, seconds, nanoseconds.
Duration twoHours = Duration.ofHours(2);
Duration thirtyMin = Duration.ofMinutes(30);
Duration fromParts = Duration.of(2, ChronoUnit.HOURS);
// Calculate duration between two instants/times
Instant start = Instant.now();
// ... work ...
Instant end = Instant.now();
Duration elapsed = Duration.between(start, end);
long ms = elapsed.toMillis();
long seconds = elapsed.getSeconds();
long minutes = elapsed.toMinutes();
// Arithmetic
Duration total = twoHours.plus(thirtyMin); // 2h30m
Duration half = twoHours.dividedBy(2); // 1h
Period
A date-based amount — years, months, days.
Period twoYears = Period.ofYears(2);
Period threeMonths = Period.ofMonths(3);
Period complex = Period.of(1, 6, 15); // 1 year, 6 months, 15 days
// Calculate period between two dates
LocalDate birthday = LocalDate.of(1990, 6, 15);
LocalDate today = LocalDate.now();
Period age = Period.between(birthday, today);
System.out.printf("Age: %d years, %d months, %d days%n",
age.getYears(), age.getMonths(), age.getDays());
// Add period to date
LocalDate future = today.plus(Period.ofMonths(3));
Key difference: Duration works with time (hours, minutes, seconds); Period works with calendar units (years, months, days). Use Period when “3 months from now” needs to account for different month lengths.
DateTimeFormatter
Replaces the non-thread-safe SimpleDateFormat.
// Built-in formatters
DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE; // 2024-01-15
DateTimeFormatter isoTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME; // 2024-01-15T14:30:00
DateTimeFormatter rfc1123 = DateTimeFormatter.RFC_1123_DATE_TIME;
// Custom pattern
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
DateTimeFormatter withLocale = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.UK);
// Formatting
String formatted = LocalDateTime.now().format(custom); // "04/05/2026 14:30"
String localeStr = LocalDate.now().format(withLocale); // "4 May 2026"
// Parsing
LocalDate date = LocalDate.parse("15/01/2024", custom);
LocalDateTime dt = LocalDateTime.parse("2024-01-15T14:30:00");
// DateTimeFormatter is thread-safe — safe as a static constant
public static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Common Pattern Symbols
| Symbol | Meaning | Example |
|---|---|---|
yyyy | 4-digit year | 2024 |
MM | 2-digit month | 01 |
MMM | Abbreviated month | Jan |
MMMM | Full month | January |
dd | 2-digit day | 15 |
HH | 24-hour hour | 14 |
hh | 12-hour hour | 02 |
mm | Minutes | 30 |
ss | Seconds | 45 |
SSS | Milliseconds | 123 |
a | AM/PM | PM |
z | Zone name | GMT |
Z | Zone offset | +0530 |
XXX | ISO zone offset | +05:30 |
Migrating from java.util.Date / Calendar
Date → Instant → LocalDateTime
// Legacy Date to LocalDateTime
Date legacy = new Date();
LocalDateTime ldt = legacy.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
// LocalDateTime to legacy Date
Date back = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
Calendar → ZonedDateTime
Calendar cal = Calendar.getInstance();
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());
SimpleDateFormat → DateTimeFormatter
// Old (not thread-safe)
private static final ThreadLocal<SimpleDateFormat> SDF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// New (thread-safe)
private static final DateTimeFormatter DTF =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
SQL Date/Timestamp interop
// java.sql.Date → LocalDate
LocalDate ld = sqlDate.toLocalDate();
// LocalDate → java.sql.Date
java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
// java.sql.Timestamp → LocalDateTime
LocalDateTime ldt = sqlTimestamp.toLocalDateTime();
// LocalDateTime → java.sql.Timestamp
java.sql.Timestamp ts = java.sql.Timestamp.valueOf(localDateTime);
Common Patterns
Check if a date is a weekend
boolean isWeekend = date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY;
Get all dates in a month
LocalDate firstDay = YearMonth.of(2024, 1).atDay(1);
LocalDate lastDay = YearMonth.of(2024, 1).atEndOfMonth();
firstDay.datesUntil(lastDay.plusDays(1)) // Java 9+
.forEach(System.out::println);
// Java 8 equivalent
LocalDate d = firstDay;
while (!d.isAfter(lastDay)) {
System.out.println(d);
d = d.plusDays(1);
}
Convert UTC timestamp to user’s local time
Instant utcTimestamp = Instant.parse("2024-06-15T10:30:00Z");
ZoneId userZone = ZoneId.of("Asia/Kolkata");
ZonedDateTime localTime = utcTimestamp.atZone(userZone);
// 2024-06-15T16:00:00+05:30[Asia/Kolkata]
Summary
| Type | Use for |
|---|---|
LocalDate | Date-only (birthdays, deadlines) |
LocalTime | Time-only (schedules, opening hours) |
LocalDateTime | Date + time, no zone (local events) |
ZonedDateTime | Date + time + zone (global scheduling) |
OffsetDateTime | Date + time + fixed offset (serialisation) |
Instant | Machine timestamp (logs, auditing) |
Duration | Time-based amount (elapsed time) |
Period | Date-based amount (age, intervals) |
DateTimeFormatter | Parsing/formatting (thread-safe) |
Next Step
Default and Static Interface Methods: Backwards-Compatible API Evolution →
Part of the DevOps Monk Java tutorial series: Java 8 → Java 11 → Java 17 → Java 21