java.time (Date and Time API)
Master Java's modern date-time API: LocalDate, LocalDateTime, Instant, Duration, Period, and ZonedDateTime for robust time handling.
Introduction
Java 8 introduced the java.time package to address the well-known deficiencies of java.util.Date and java.util.Calendar. The new API is immutable, thread-safe, and follows a clear domain-driven design with separate types for different time concepts.
When to Use
| Use Case | Recommended Type |
|---|---|
| Birthdays, holidays, birthdays | LocalDate |
| Timestamp with no timezone context | Instant |
| Precise elapsed durations | Duration |
| Age calculation, calendar periods | Period |
| Time with timezone awareness | ZonedDateTime / OffsetDateTime |
| Human-readable date and time | LocalDateTime |
When NOT to Use
- Legacy interop: Use
java.util.Dateonly when forced by legacy APIs. - Database columns: Use JDBC adapters rather than manual conversion; prefer
LocalDateTimeforTIMESTAMP WITH TIME ZONE. - Serializing to JSON: Configure
ObjectMapperwithJavaTimeModuleinstead of manual parsing.
Core Types Diagram
classDiagram
class Temporal~T~ {
<<interface>>
}
class TemporalAmount {
<<interface>>
}
class LocalDate {
+now() LocalDate
+of(int year, int month, int day) LocalDate
+plusDays(long) LocalDate
+minusDays(long) LocalDate
+isAfter(LocalDate) boolean
}
class LocalDateTime {
+now() LocalDateTime
+of(int year, int month, int day, int hour, int min) LocalDateTime
+toLocalDate() LocalDate
+toLocalTime() LocalTime
}
class Instant {
+now() Instant
+ofEpochSecond(long) Instant
+plus(Duration) Instant
+minus(Duration) Instant
+toEpochMilli() long
}
class ZonedDateTime {
+now(ZoneId) ZonedDateTime
+of(LocalDateTime, ZoneId) ZonedDateTime
+withZoneSameInstant(ZoneId) ZonedDateTime
}
class Duration {
+ofDays(long) Duration
+ofHours(long) Duration
+between(Temporal, Temporal) Duration
+toMillis() long
}
class Period {
+ofDays(int) Period
+ofMonths(int) Period
+between(LocalDate, LocalDate) Period
+getDays() int
}
Temporal~T~ <|-- LocalDate
Temporal~T~ <|-- LocalDateTime
Temporal~T~ <|-- Instant
Temporal~T~ <|-- ZonedDateTime
TemporalAmount <|-- Duration
TemporalAmount <|-- Period
LocalDateTime --> LocalDate : toLocalDate()
ZonedDateTime --> LocalDateTime : composes
Code Examples
LocalDate — Birthdays and Calendar Dates
import java.time.LocalDate;
import java.time.Month;
LocalDate today = LocalDate.now();
LocalDate javaBirthday = LocalDate.of(1995, Month.MAY, 23);
LocalDate endOfYear = LocalDate.of(2026, 12, 31);
// Difference in days
long daysBetween = javaBirthday.until(today).getDays();
System.out.println("Java is " + daysBetween + " days old");
// Adding/subtracting
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
// Comparison
if (today.isAfter(javaBirthday)) {
System.out.println("Java has been around for a while");
}
Instant — Machine Timestamps
import java.time.Instant;
import java.time.Duration;
Instant startup = Instant.now();
// ... app runs ...
Instant now = Instant.now();
Duration elapsed = Duration.between(startup, now);
System.out.println("Elapsed: " + elapsed.toMillis() + "ms");
// Convert to/from epoch millis
long epochMillis = Instant.now().toEpochMilli();
Instant restored = Instant.ofEpochMilli(epochMillis);
ZonedDateTime — Timezone-Aware Timestamps
import java.time.ZoneId;
import java.time.ZonedDateTime;
ZonedDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC"));
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// Convert between zones
ZonedDateTime parisScheduled = ZonedDateTime.of(2026, 6, 15, 14, 0, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime tokyoConverted = parisScheduled.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Paris 14:00 = Tokyo " + tokyoConverted.getHour() + ":00");
Duration and Period — Elapsed Time
import java.time.Duration;
import java.time.Period;
import java.time.LocalDate;
Duration oneHour = Duration.ofHours(1);
Duration halfHour = Duration.ofMinutes(30);
// Combine durations
Duration total = oneHour.plus(halfHour); // 90 minutes
// Period for calendar math
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 5, 23);
Period age = Period.between(start, end);
System.out.println("Months: " + age.getMonths() + ", Days: " + age.getDays());
Date Formatting and Parsing
import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;
DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");
LocalDateTime now = LocalDateTime.now();
String formatted = now.format(custom);
System.out.println(formatted); // e.g., 23/05/2026 14:30
LocalDateTime parsed = LocalDateTime.parse("23/05/2026 14:30", custom);
Failure Scenarios
| Scenario | Problem | Solution |
|---|---|---|
LocalDate.of(2026, 2, 30) | DateTimeException — invalid day for February | Validate day range: day > 0 && day <= month.length(year % 4 == 0) |
Instant.parse("2026-02-30T10:00") | DateTimeParseException | Always parse through a formatter with lenient mode if input is untrusted |
Duration.between(date1, date2) | java.time.temporal.UnsupportedTemporalTypeException | Duration works with Instant, LocalDateTime, OffsetTime; use Period for LocalDate |
ZonedDateTime.now(ZoneId.of("Mars")) | ZoneRulesException | Validate zone ID against ZoneId.getAvailableZoneIds() |
Race condition on cached ZoneId | Stale zone data | Do not cache ZoneId instances across JVM restarts |
Trade-off Table
| Aspect | java.time | java.util.Date | java.util.Calendar |
|---|---|---|---|
| Thread safety | Immutable, thread-safe | Mutable, not thread-safe | Mutable, not thread-safe |
| API clarity | Domain-specific types | Single ambiguous type | Complex, inconsistent |
| Timezone handling | First-class ZoneId | Manual offset management | Partial support |
| Performance | Slight overhead for immutability | Fast but unsafe | Slow and unsafe |
| New features | Streams, parsing, formatting | Legacy only | Legacy only |
Observability Checklist
// Instrumenting time operations for observability
import java.time.Instant;
import java.time.Duration;
public class TimedOperation {
private final Instant start;
public TimedOperation() {
this.start = Instant.now();
}
public long elapsedMillis() {
return Duration.between(start, Instant.now()).toMillis();
}
// Log with structured fields for observability platforms
public void logDuration(String operation) {
System.out.println("operation=" + operation +
" duration_ms=" + elapsedMillis() +
" timestamp=" + Instant.now());
}
}
- Measure elapsed time with
Duration.between(start, end)and expose as a metric. - Use
Instantfor log timestamps rather thanSystem.currentTimeMillis(). - Validate timezone IDs at startup to fail fast on misconfiguration.
- Store
Instantin persistence layers, notLocalDateTime, to preserve the moment in time. - Log parsing failures with the raw input string for debugging.
Security Notes
- Time-of-check to time-of-use (TOCTOU): Immutable types eliminate some TOCTOU races but calendar operations still require atomic validation.
- Timezone spoofing: If user input sets the timezone, validate it against an allowlist of permitted zones.
- Deserialization attacks: Malformed date strings in untrusted input can cause
DateTimeParseException; catch and log securely without leaking stack traces. - Leap second handling:
Instanttreats leap seconds as a 1-second increment, not as a special case — do not rely onInstantfor financial time calculations.
Pitfalls
- Confusing
DurationwithPeriod:Duration.ofDays(1)creates a 24-hour duration, whilePeriod.ofDays(1)represents a calendar day which may have different length near DST transitions. - Forgetting
ZonedDateTimefor scheduled tasks:LocalDateTime.now()returns a time without timezone — two users in different zones see different “now”. - Immutability surprises: Methods like
plusDays()return a new instance — the original is unchanged. Assign the result. - Epoch overflow:
Instant.MAXis year+1,000,000,000;Instant.MINis year-1,000,000,000. Unlikely to hit, but be aware. LocalDateTimetoZonedDateTimeis ambiguous:LocalDateTime.of(2026, 3, 29, 2, 30)during DST spring-forward has no valid UTC equivalent — this throws an exception.
Quick Recap
java.timeis immutable, thread-safe, and domain-driven.LocalDatefor calendar dates,LocalDateTimefor naive datetime,Instantfor UTC timestamps.ZonedDateTimefor timezone-aware scheduling; useZoneIdto define the zone.Durationfor precise elapsed time,Periodfor calendar-based elapsed time.- Format with
DateTimeFormatter; parse withLocalDateTime.parse(str, formatter). - Always use
Instantfor logging and metric timestamps. - Prefer
Duration.between()over manual subtraction for elapsed time.
Interview Questions
Model Answer: "`Instant` represents a point in time on the UTC timeline (nanosecond precision, epoch seconds since `1970-01-01T00:00:00Z`). `LocalDateTime` represents a local date and time with no timezone information — it is the same moment viewed differently in different zones. Use `Instant` for machine timestamps and logging; use `LocalDateTime` for human-readable calendar events that have no inherent timezone."
Model Answer: "`Duration` models a precise amount of time in seconds or nanoseconds — it is suited for measuring elapsed time between two `Instant` values. `Period` models a calendar-based amount in days, months, and years — it is suited for date arithmetic where you want "1 month later" to roll over to the next month calendar-wise. A `Duration` of one day is always exactly 86,400 seconds; a `Period` of one day may represent a different amount of wall-clock time near DST transitions."
Model Answer: "A `DateTimeParseException` is thrown at runtime. The `java.time` types perform strict validation — there is no silent rollover or clamping like the legacy `Calendar`. You must validate input before parsing or catch the exception explicitly."
Model Answer: "Use `ZonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))` to convert a `ZonedDateTime` to a different zone while preserving the same instant in time. Use `ZonedDateTime.withZoneSameLocal()` if you want to keep the local time but change the zone (this can produce different instants near DST transitions). For `Instant`, use `Instant.atZone(ZoneId)` to attach a zone."
Model Answer: "`java.util.Date` is mutable and not thread-safe, its `toString()` returns a misleading local timezone string, and month numbering is 0-based. You still need it only for interoperability with legacy APIs that require `java.util.Date` — JDBC `setTimestamp()`, some enterprise integrations, and `java.util.concurrent.ScheduledExecutorService`. In all new code, use `java.time`."
Model Answer: "`ZonedDateTime` includes a time zone ID (e.g., "Europe/Paris") which represents the full timezone rules including DST transitions. `OffsetDateTime` includes only a fixed UTC offset (e.g., "+02:00") which does not account for DST. A `ZonedDateTime` knows the exact wall-clock time in a region; an `OffsetDateTime` only knows the offset from UTC. Use `ZonedDateTime` for scheduled events in a region; use `OffsetDateTime` for timestamps where the offset is known but zone rules are not needed."
Model Answer: "`Instant.now()` uses the system clock in UTC — it returns the same instant value across all JVMs on the same machine if they share the same clock source. However, different machines may have slightly different clock readings. For distributed system correlation, use a centralized time source (NTP) or log both wall-clock time and epoch millis. Never assume two JVMs have synchronized clocks — always use epoch-based timestamps (`Instant.toEpochMilli()`) for cross-system correlation."
Model Answer: "`TemporalAdjusters` provides common date adjustments: `nextOrSame(DayOfWeek)`, `lastDayOfMonth()`, `firstDayOfNextMonth()`, etc. They implement the `TemporalAdjuster` interface and can be passed to `with()` methods on date-time types. For example: `date.with(TemporalAdjusters.lastDayOfMonth())` returns the last day of the month. Use these instead of manual day-of-month calculations for readability and correctness near month boundaries."
Model Answer: "`LocalDate.of(2026, 1, 31).plusMonths(1)` throws `DateTimeException` because February has no 31st day. The design intentionally prevents silent rollover. Options: use `plusDays()` for a fixed day count, use `withDayOfMonth()` after adding months to clamp to the valid day, or check the day range beforehand and decide whether to throw or clamp. The same rule applies to `minusMonths()`, `plusYears()`, etc."
Model Answer: "A `Duration` of one day is always exactly 86,400 seconds (24 hours). A `Period` of one day represents a calendar day — near DST transitions, a day may have 23 or 25 hours. For example, adding a `Period` of one day across a spring-forward DST transition produces a different wall-clock result than adding a `Duration` of one day. Use `Period` for human-calendar arithmetic; use `Duration` for precise elapsed time measurement."
Model Answer: "`ZoneId.of("UTC")` returns the "UTC" zone ID (a `ZoneId` with fixed offset of 0). `ZoneOffset.UTC` is a `ZoneOffset` representing UTC with zero offset. Both represent UTC but have different types — `ZoneOffset` is a simple fixed offset, while `ZoneId` is a full timezone with rules. When you need UTC specifically, `ZoneOffset.UTC` is cleaner; when you need a full `ZoneId` for methods requiring a zone, use `ZoneId.of("UTC")`."
Model Answer: "`LocalDateTime.parse(CharSequence)` uses `DateTimeFormatter.ISO_LOCAL_DATE_TIME` by default. If the input string is malformed, it throws `DateTimeParseException`. For untrusted input, wrap parsing in a try-catch or use `DateTimeFormatter` with a custom pattern and `setResolverStyle(ResolverStyle.STRICT)` for stricter validation. For user-facing input that may be flexible, use `DateTimeFormatter` with `ResolverStyle.SMART` or `LENIENT` as appropriate."
Model Answer: "`Instant` is the recommended type for log timestamps and metric events because it represents a point in time globally (UTC-based) without timezone ambiguity. `Instant.now()` is monotonic and well-suited for elapsed time calculation. Unlike `LocalDateTime` which is timezone-specific and can be ambiguous near DST transitions, `Instant` always refers to the same moment. Use `Instant` when the exact moment matters; use `ZonedDateTime` when the wall-clock display matters."
Model Answer: "`Period.between(LocalDate start, LocalDate end)` returns a `Period` with years, months, and days — appropriate for calendar arithmetic. `Duration.between(Instant start, Instant end)` returns a `Duration` with seconds and nanoseconds — appropriate for precise elapsed time. If you call `Duration.between()` on two `LocalDate` values, it throws an exception because `LocalDate` is not a `Temporal` type that `Duration.between()` accepts."
Model Answer: "During the hour "skipped" by spring-forward (e.g., 2:00 AM becomes 3:00 AM in US), times in that gap are invalid and throw `DateTimeException`. During the hour "repeated" by fall-back (e.g., 1:00 AM happens twice), the later offset is used by default. `ZonedDateTime.ofLocal()` handles these by using the offset after the transition for ambiguous times and throwing for gaps. `ZonedDateTime.ofInstant()` can recreate any ambiguous time by specifying the offset explicitly."
Model Answer: "`DateTimeFormatter.ofLocalizedDateTime()` uses locale-specific formatting — for example, `FormatStyle.MEDIUM` produces "Jan 23, 2026" in US locale but "23 Jan 2026" in UK locale. The ISO formatters (`DateTimeFormatter.ISO_LOCAL_DATE_TIME`, etc.) always use the ISO format regardless of locale. Use localized formatters for displaying dates to users in their locale; use ISO formatters for machine-to-machine communication and storage."
Model Answer: "These create a `Period` with only the specified unit set — `Period.ofDays(5)` creates a period of 5 days with 0 months and 0 years. They are more explicit than `Period.of(5, 0, 0)` because they document intent immediately. Use them when you need a simple period of a single unit. `Period.parse("P5D")` parses ISO-8601 duration format and produces the same result."
Model Answer: "`TemporalUnit.between(Temporal start, Temporal end)` is the general-purpose method on the `TemporalUnit` interface (e.g., `ChronoUnit.DAYS.between(start, end)`) and can compute the difference between any two temporal types, returning the appropriate unit as a `long`. It handles the type conversion — for example, `ChronoUnit.DAYS.between(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 31))` returns 30. `Duration.between()` and `Period.between()` are specialized methods for specific temporal types."
Model Answer: "Both return the current date in the specified zone. LocalDate.now(ZoneId) is the cleaner API — it directly returns the date portion of the current instant in that zone. ZonedDateTime.now(ZoneId).toLocalDate() creates an intermediate ZonedDateTime object and then extracts the date from it. The result is identical, but LocalDate.now(ZoneId) is more efficient and readable."
Model Answer: "instant.plusDays(1) does not exist on Instant — Instant only supports plus(long, TemporalUnit) or plus(Duration). instant.plus(Duration.ofDays(1)) works because Duration implements TemporalAmount. For Instant, use instant.plus(1, ChronoUnit.DAYS) or instant.plus(Duration.ofDays(1)). For LocalDate, plusDays(int) is the standard method. Mixing them can cause confusion — check the type before calling methods."
Further Reading
- MDN: Working with dates and times in JavaScript — cross-language perspective on temporal handling pitfalls
- Baeldung: Java Time API Guide — comprehensive coverage of
java.timepatterns - Oracle: Date Time Tutorial — official Java documentation for date-time fundamentals
- Stack Overflow: java.time ZonedDateTime DST handling — common DST edge cases and solutions
- JEP 302: Lambda Leftovers — background on Java evolution influencing
java.timedesign decisions
Conclusion
The java.time package addresses everything wrong with java.util.Date and java.util.Calendar — mutability, thread-safety issues, confusing API design, and zero-based month indexing. The new API is immutable by design, which makes it safe to share across threads and predictable across time-zone boundaries. Once you internalize the domain model (LocalDate for calendar days, Instant for machine timestamps, ZonedDateTime for scheduled events), you will find date-time code becomes significantly shorter and less bug-prone.
The most important decision in java.time is choosing the right type for the job. LocalDate stores a calendar date with no time component — use it for birthdays, expiration dates, and any date that is inherently calendar-aligned. Instant stores a point in UTC time — use it for log timestamps, metric events, and any moment that must be preserved across time zones. ZonedDateTime adds a time zone to a local date-time — use it for scheduled events that must fire at a specific wall-clock time in a specific region.
Duration vs Period is another critical distinction. Duration measures elapsed time in seconds and nanoseconds between two TemporalAccessor points — it is appropriate for measuring program execution time, latency metrics, and any time interval that is precisely defined. Period measures elapsed time in calendar units (days, months, years) between two LocalDate values — use it for age calculations, subscription periods, and any interval where calendar semantics matter more than precise milliseconds.
For formatting and parsing, use java.time.format.DateTimeFormatter with the patterns from java.time rather than the legacy java.text formatting classes. DateTimeFormatter is immutable and thread-safe, whereas SimpleDateFormat is neither. The pattern syntax is similar but the new API is more consistent and supports localization properly.
- Use
LocalDatefor birthdays, holidays, and any date without a time component - Use
Instantfor all machine timestamps — log timestamps, metric events, and event sourcing - Use
ZonedDateTimefor scheduled events that must fire at a specific wall-clock time - Use
Durationfor precise elapsed time between instants; usePeriodfor calendar-based intervals - Always use
DateTimeFormatterfromjava.time.format— neverSimpleDateFormatfor new code java.timetypes are immutable — methods likeplusDays()return new instances, they do not mutate
Category
Related Posts
Abstract Classes in Java
Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.
Arithmetic Operators in Java
Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.
Array Basics in Java
Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.