skip to content

How do you convert between the legacy java.util.Date / Calendar and the modern java.time types, and what subtleties matter?

level: middleimportance: must knowfreq 60%

answer

  1. Date.from(instant) / date.toInstant()
  2. java.util.Date == an Instant (millis, UTC), not a calendar date
  3. GregorianCalendar.from(zdt) / cal.toInstant()/toZonedDateTime()
  4. java.sql.Date.toInstant() throws; use toLocalDate()
  5. Timestamp keeps nanos; util.Date is millis-only

basics

~10 s

Use the bridge methods added in Java 8: date.toInstant() and Date.from(instant); calendar.toInstant() and GregorianCalendar.from(zonedDateTime). A java.util.Date is really an instant (UTC), so it maps cleanly to Instant, not to LocalDateTime.

solid answer

~40 s

Java 8 added bridge methods so you don't have to do manual epoch math. java.util.Date is, despite its name, a millisecond-precision point on the timeline (an instant), so it maps to Instant: date.toInstant() and Date.from(instant). Going to a human/zoned view you then attach a zone: date.toInstant().atZone(zone). For Calendar, use GregorianCalendar (cal.toInstant(), GregorianCalendar.from(zdt)); a generic Calendar offers getTime()/toInstant() too. Subtleties: java.sql.Date/Time/Timestamp subclass java.util.Date but have their own bridges (Timestamp.toInstant()/from, Date.toLocalDate()) and Timestamp keeps nanos while util.Date is millis-only — calling toInstant() on a java.sql.Date throws. Date is mutable and not thread-safe, which is one reason to migrate. Never reuse Date's deprecated getYear()/getMonth(); convert and use java.time.

code

java · 14 lines
java
// java.util.Date <-> Instant
Date legacy = new Date();
Instant instant = legacy.toInstant();
Date back = Date.from(instant);

// Human view via a zone
ZonedDateTime zdt = legacy.toInstant().atZone(ZoneId.systemDefault());

// java.sql.Timestamp keeps nanos
Timestamp ts = Timestamp.from(instant);
Instant lossless = ts.toInstant();

// java.sql.Date is date-only: use toLocalDate, NOT toInstant
LocalDate day = java.sql.Date.valueOf("2026-06-20").toLocalDate();

go deeper

for a junior

Knows the bridge methods exist (Date.from / toInstant) and uses them instead of manual millis math.

for a middle

Understands java.util.Date is an instant (not a calendar date), maps it to Instant, and adds a zone for human views.

for a senior

Handles java.sql.* subtleties (own bridges, toInstant throwing on sql.Date, Timestamp nanos), and keeps legacy types only at boundaries.

for a principal

Drives a migration strategy: where Date is allowed to live, anti-corruption boundaries around legacy/JDBC code, and immutability/thread-safety rationale across the codebase.

## Two generations of date APIs Before Java 8 the JDK had **`java.util.Date`** and **`java.util.Calendar`**. They were error-prone: mutable, not thread-safe, zero-based months, badly named. Java 8 introduced the immutable **`java.time`** package (JSR-310). Because mountains of old code and libraries still produce `Date`/`Calendar`, the JDK added **bridge methods** so you can convert at the boundary instead of doing manual epoch arithmetic. ## What a `java.util.Date` actually is Key insight: despite "Date" in the name and a `toString()` that prints a zone, a `java.util.Date` holds **only a millisecond count since the epoch** — it is a **single instant in UTC**, not a calendar date and not zoned. So its natural modern counterpart is **`Instant`**, not `LocalDate`/`LocalDateTime`. ```java Date legacy = new Date(); Instant instant = legacy.toInstant(); // Date -> Instant Date again = Date.from(instant); // Instant -> Date ``` To get a human, zoned view you add a zone *after* converting: ```java ZonedDateTime zdt = legacy.toInstant().atZone(ZoneId.systemDefault()); LocalDate day = legacy.toInstant().atZone(zone).toLocalDate(); ``` The **`Date(year, month, day)`** constructors and `getYear()/getMonth()` getters are deprecated and use a 1900-based year / 0-based month — never use them; convert to `java.time` and read the proper fields. ## Calendar `Calendar` is the legacy mutable, zone-aware calendar. The common concrete type is **`GregorianCalendar`**, which has direct bridges: ```java GregorianCalendar gcal = new GregorianCalendar(); Instant i = gcal.toInstant(); ZonedDateTime zdt = gcal.toZonedDateTime(); GregorianCalendar back = GregorianCalendar.from(zdt); // preserves the zone ``` A plain `Calendar` reference exposes `getTime()` (→ `Date`) and `toInstant()`. ## java.sql.* subtleties (a classic trap) `java.sql.Date`, `java.sql.Time`, and `java.sql.Timestamp` all **extend `java.util.Date`** but represent narrower concepts and have their **own** bridges — and some inherited methods are *deliberately broken*: - **`java.sql.Date`** = a date only. Use `sqlDate.toLocalDate()` / `java.sql.Date.valueOf(localDate)`. Its inherited **`toInstant()` throws `UnsupportedOperationException`** because a pure date isn't an instant. - **`java.sql.Time`** = a time of day. Use `toLocalTime()` / `Time.valueOf(localTime)`; `toInstant()` also throws. - **`java.sql.Timestamp`** = an instant with **nanosecond** precision. Use `ts.toInstant()` / `Timestamp.from(instant)` and `toLocalDateTime()` / `valueOf(ldt)`. Unlike `java.util.Date` (millis only), Timestamp **preserves nanos**, so a Timestamp↔Instant round-trip is lossless. Because `java.sql.*` are subclasses of `java.util.Date`, passing one where a `java.util.Date` is expected compiles fine but can blow up at runtime when the wrong bridge is invoked — convert deliberately to the *right* `java.time` type. ## Why migrate at all `Date`/`Calendar` are **mutable** (a method can change your shared instance) and **not thread-safe**; `java.time` types are immutable and thread-safe. Also `SimpleDateFormat` is not thread-safe, whereas `DateTimeFormatter` is. The bridge methods let you keep `Date` only at the unavoidable edges (old libraries, JDBC drivers predating JDBC 4.2) and work with `java.time` everywhere inside. ## Mental model Treat `java.util.Date` as "a poorly-named `Instant`". Convert it to `Instant` the moment it enters your code, do all logic in `java.time`, and convert back to `Date` only if some legacy API demands it.

  • Why does java.sql.Date.toInstant() throw while java.sql.Timestamp.toInstant() works?
    java.sql.Date models a date with no time-of-day, so it cannot be a single instant and overrides toInstant() to throw; Timestamp models an instant with nanosecond precision, so its toInstant() is meaningful and lossless.
  • How do you turn a java.util.Date into a LocalDate for a specific zone?
    date.toInstant().atZone(zone).toLocalDate() — convert to Instant, apply the zone to interpret the wall-clock day, then drop to LocalDate.

saying these in an interview costs you the question

  • Mapping java.util.Date to LocalDateTime as if it had no zone meaning
  • Calling toInstant() on a java.sql.Date (throws)
  • Using deprecated Date.getYear()/getMonth()
  • Assuming util.Date keeps nanoseconds
  • Sharing a mutable Date across threads

context