Interoperating with Legacy Dates
Interoperating with Legacy Dates
Real-world Java code rarely starts from a blank slate. You will encounter APIs, libraries, databases, and serialisation layers that speak the old java.util.Date and java.util.Calendar dialect. The java.time package was designed with explicit bridge methods so that migration can happen incrementally — you do not have to rewrite everything at once.
This lesson covers every direction of conversion, the gotchas hiding in time-zone semantics, and the practical pattern for deciding when to convert versus when to leave the old type in place.
A Quick Map of the Old World
Before you can bridge the two worlds you need to know what maps to what:
java.util.Date— a millisecond timestamp since the Unix epoch (1970-01-01T00:00:00Z). Despite the name it has no concept of a calendar date; it is really a wrapper around along.java.util.Calendar/GregorianCalendar— a mutable, time-zone-aware representation that combines a date, a time, and aTimeZone.java.sql.Date,java.sql.Time,java.sql.Timestamp— JDBC types that extendjava.util.Date; they carry extra precision but the same epoch-millis foundation.
TimeZone on top. java.time makes this contract explicit rather than hiding it.
java.util.Date to java.time
Date gained a single bridge method: toInstant(). Because Date is nothing more than a millisecond counter, the natural target is Instant. From there you project into any zone-aware or local type you need.
ZoneId.systemDefault() without thinking. On a server the default zone is usually UTC or the OS zone, which changes between environments. Be explicit about the target zone; otherwise conversions silently produce different local dates on different machines.
java.time to java.util.Date
The reverse direction uses the static factory Date.from(Instant). If your source is a zone-aware type convert to Instant first.
LocalDate or LocalDateTime always requires a zone. These types intentionally have no time-zone information. If you omit the zone you are forced to choose one — make that choice visible in the code, not hidden in a helper method.
Calendar and GregorianCalendar
GregorianCalendar is the concrete class you almost always receive when working with Calendar. It has a direct conversion method.
The bridge preserves the original TimeZone exactly, so a Calendar in Asia/Tokyo becomes a ZonedDateTime with Asia/Tokyo — no silent zone shift.
Calendar subclass for non-Gregorian calendars: JapaneseImperialCalendar (used when the JVM locale is Japanese). For those cases extract the epoch millis manually: cal.getTimeInMillis(), then wrap in Instant.ofEpochMilli(...).
JDBC Types: java.sql.Date, Time, and Timestamp
JDBC types are the most common source of legacy dates in backend applications. Each has a direct toLocalXxx() bridge — notice these do not go through Instant because JDBC dates are explicitly zone-less at the protocol level.
Timestamp.valueOf(LocalDateTime) over Timestamp.from(Instant) when writing to a database column typed as DATETIME (not TIMESTAMP WITH TIME ZONE). The valueOf path keeps the literal calendar fields; the from path applies the JVM default zone, which can silently shift values in MySQL and similar databases.
A Practical Migration Pattern
When you inherit a large codebase you cannot always rewrite every layer at once. A safe incremental strategy is:
- Convert at the boundary. Leave internal model classes and DTOs using
java.time. Convert to/from legacy types only at the I/O boundary — the point where you call an old library or read from JDBC. - Write a dedicated adapter method. Centralise the conversion logic. When you later upgrade the old library you change one place.
- Annotate with
@SuppressWarnings("deprecated")sparingly. Keep legacy conversion code isolated so it is easy to delete once the old dependency is gone.
Summary
Every legacy type has a clean bridge into java.time:
Date.toInstant()/Date.from(Instant)— the universal gateway.GregorianCalendar.toZonedDateTime()/GregorianCalendar.from(ZonedDateTime)— preserves the original time zone.java.sql.Date.toLocalDate(),Time.toLocalTime(),Timestamp.toLocalDateTime()— zone-less JDBC paths.
The golden rule is always be explicit about time zones during conversion. The errors that plagued the old API were almost all caused by implicit zone assumptions — java.time forces you to state those assumptions in code.