W3docs

Java Date and Time API Introduction

An introduction to the modern Java date and time API in java.time, replacing the legacy Date and Calendar classes.

Java Date and Time API Introduction

Java 8 added java.time, a new package for representing dates, times, durations, time zones, and the arithmetic between them. It replaced two previous APIs — java.util.Date and java.util.Calendar — that had a deserved reputation as the worst-designed corner of the JDK. The new API was driven by Stephen Colebourne's earlier Joda-Time library; if you've used Joda, java.time will feel familiar.

The two important things about the redesign:

  1. Every type is immutable. A LocalDate once created never changes. Methods like plusDays(7) return a new LocalDate. That makes the API thread-safe by construction and removes a whole category of bugs.
  2. Each type means one thing. LocalDate is a date with no time. Instant is a moment on the timeline. Duration is a length of time. The legacy Date was somehow all of these at once, depending on which constructor you used; the new API splits them apart so the type tells you what kind of value you have.

This chapter is the map. The next ten chapters drill into each class.

The core types

"A date"           LocalDate            2025-11-04
"A time of day"    LocalTime            14:30:00
"Both, no zone"    LocalDateTime        2025-11-04T14:30:00
"Both, with zone"  ZonedDateTime        2025-11-04T14:30:00-05:00 [America/New_York]
"A moment"         Instant              2025-11-04T19:30:00Z      (UTC, seconds-since-epoch)
"A length of time" Duration             PT1H30M                   (1 hour 30 minutes)
"A length of date" Period               P1Y2M3D                   (1 year 2 months 3 days)

The horizontal split — Local* vs Zoned/Instant — is the most important one. Local types carry no time zone. A LocalDate of 2025-11-04 is "the fourth of November"; it does not say whether that's the fourth in Tokyo or in Honolulu. It's the right type for a birthday, a contract date, or a UI date picker.

Zoned types carry their zone. ZonedDateTime is "this calendar instant in this place," which is what you want for "meeting scheduled for 9 AM in New York." Instant is a moment on the global timeline — UTC seconds since the epoch — which is what you want for logging, message timestamps, anything that needs to be globally ordered without needing local labels.

The horizontal split between Duration and Period matters too. Duration is a length of time you can compare in seconds — PT24H is exactly 24 × 3600 seconds. Period is a length expressed in calendar terms — P1M (one month) is 30 days in some months and 31 in others. For timing measurements, you want Duration. For "add one month to a billing date," you want Period.

The fluent shape

Every type is built and modified through a consistent fluent API:

LocalDate today    = LocalDate.now();
LocalDate stardate = LocalDate.of(2025, 11, 4);
LocalDate parsed   = LocalDate.parse("2025-11-04");

LocalDate nextWeek = today.plusDays(7);                     // immutable: returns a NEW LocalDate
LocalDate lastYear = today.minusYears(1);
LocalDate firstOfMonth = today.withDayOfMonth(1);            // with* returns a copy with one field changed

boolean before = today.isBefore(stardate);
int year = today.getYear();

Three shapes you'll see everywhere:

  • now() — current value from the system clock.
  • of(...) — explicit components.
  • parse(...) — from a string (ISO-8601 by default).

And for transformations:

  • plusX(n) / minusX(n) — arithmetic.
  • withX(value) — replace a single field.
  • isBefore(other) / isAfter(other) — comparison.

This shape repeats across LocalDate, LocalTime, LocalDateTime, ZonedDateTime, and Instant. Once you know the pattern, every class is reading you the same dialect with a slightly different vocabulary.

Time zones are hard, and the API admits it

The single biggest reason java.util.Date was painful is that it tried to make time zones invisible. The result was the famous bug class of "store a Date, retrieve it on a server in a different time zone, get the wrong calendar date." java.time solves this by making the zone explicit in the type.

If you accept a date from a user and don't know what zone they're in, store it as a LocalDate. If they tell you it's "9 AM their time" and you know their zone, store it as a ZonedDateTime with the zone. If you log a server event, store it as an Instant. Don't store a LocalDateTime and hope the time zone comes through; the missing zone is the whole bug.

Instant now = Instant.now();                                 // unambiguous: a moment in UTC
ZonedDateTime localized = now.atZone(ZoneId.of("Europe/Berlin"));  // a label for that moment in Berlin

The zone hierarchy:

  • ZoneOffset is a fixed ±HH:MM offset from UTC: +05:30, -08:00. No DST handling.
  • ZoneId is a named zone: Europe/Berlin, America/New_York. Carries the IANA database's record of what offset that zone has on any given day, including DST transitions and historical changes.

Always prefer ZoneId over ZoneOffset when you have a choice. "America/New_York" is correct across DST; "−05:00" is correct only outside DST.

The legacy types haven't gone away

java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat still exist. New code shouldn't use them — but plenty of old code does, and you'll need to interoperate. The conversion methods are direct:

// java.util.Date <-> java.time.Instant
Instant inst = legacyDate.toInstant();
Date    back = Date.from(inst);

// java.util.Calendar -> java.time.ZonedDateTime
ZonedDateTime zdt = ZonedDateTime.ofInstant(
    cal.toInstant(), cal.getTimeZone().toZoneId());

The pattern is one-way: legacy → java.time is straightforward; for anything new, stay in java.time and convert only at the API boundary where the old code lives. The Legacy Date and Calendar chapters at the end of this part cover the bridge in detail.

A worked example: the family of types in one program

The program below uses every type the map above introduced — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period — and shows how they convert into one another. It's the "tour" version; each individual type gets its own chapter from here on.

java— editable, runs on the server

What to take from the run:

  • The first block built up the family by composition: LocalDate + LocalTime = LocalDateTime; LocalDateTime + ZoneId = ZonedDateTime; ZonedDateTimeInstant. That's the lattice of conversions, and you'll do these every time you cross an API boundary. The arrows go both ways for most pairs — Instant.atZone(zone) and ZonedDateTime.toLocalDateTime() close the loops.
  • One Instant printed three different "looking" times when viewed from New York, Berlin, and Tokyo. That's the point of Instant: it's the moment, independent of where you're standing. The ZonedDateTime adds the "where I'm standing" label. Confusing the two is the legacy Date mistake.
  • Duration printed as PT1H30M and Period printed as P3M. The ISO-8601 duration format is PnYnMnDTnHnMnS — anything before the T is calendar units (Period), anything after is time units (Duration). The string is verbatim what toString() returns, and verbatim what parse(...) accepts.
  • today.plusDays(7) produced a different LocalDate. Printing today again right after showed the original was unchanged — that's the immutability guarantee. Every plus/minus/with returns a new object; the receiver is never modified. No defensive copying, no thread-safety concerns, ever.
  • ChronoUnit.DAYS.between(today, launch) was the "distance" operation. It returns a long, not a Period, because the answer in days has no calendar ambiguity (unlike months, which vary in length). Every chapter in this part uses ChronoUnit somewhere — it's the catalog of time units the API talks about.

What's next

The next chapter, Java LocalDate, starts the depth tour. LocalDate is the simplest of the five "point in time" types and the right place to learn the fluent shape that all the others share.

Practice

Practice

You need to store the moment a server received an HTTP request, so that the log can be sorted globally across servers in different time zones. Which `java.time` type fits?