☕ Java
Legacy Date API
The legacy date API refers to java.util.Date and its supporting classes (java.text.SimpleDateFormat, java.sql.Date/Time/Timestamp) that predate java.time entirely, and were never fixed in place but rather superseded wholesale by the Java 8 redesign. This entry covers the specific, concrete design flaws in java.util.Date that motivated the rewrite — its misleading name versus what it actually stores, its mutability, its non-thread-safe formatter counterpart, and its confusing, largely-deprecated field-access API — and how those legacy types interoperate with java.time for code that still has to deal with both.
What java.util.Date Actually Is — and Why Its Name Is Misleading
Despite its name, java.util.Date does not represent a date at all in the sense LocalDate does — internally it stores a single long value: the number of milliseconds since the epoch (midnight, January 1, 1970, UTC), making it, in substance, a full timestamp-like instant rather than a calendar date. This means every Date object, even one ostensibly created to represent "just a date" like someone's birthday, silently carries an implicit time-of-day and an implicit time zone interpretation baked into that millisecond count, whether or not the code creating it ever intended for a time-of-day to be meaningful at all — there was, and is, no separate java.util type for a genuinely date-only value the way LocalDate provides, which routinely forced legacy code to either ignore the unwanted time component entirely or actively zero it out by convention, a workaround prone to being forgotten or done inconsistently across a codebase.
Most of Date's field-level constructors and accessor methods (the constructor accepting year/month/day/hour/minute/second, and methods like getYear(), getMonth(), getDate(), getHours()) were deprecated as far back as Java 1.1, replaced at the time by the equivalent methods on java.util.Calendar — meaning java.util.Date has effectively been a "do not use most of its own API" class for the vast majority of its existence in the language, with Calendar serving as the only non-deprecated way to do basic field manipulation prior to java.time's arrival in Java 8.
Java
// ── Date's name is misleading — it's really just an instant in milliseconds
Date now = new Date();
System.out.println(now.getTime()); // a single long: milliseconds since the epoch
// There is NO separate field for "just the date part" — Date IS the millisecond count,
// full stop, with a time-of-day and implicit zone baked in whether wanted or not
// ── Deprecated since Java 1.1 — yet still technically callable ────────
Date deprecatedConstructed = new Date(2024 - 1900, 5, 15); // year is OFFSET from 1900!!
System.out.println(deprecatedConstructed.getYear()); // 124 — ALSO offset from 1900
// Both the constructor AND getYear() use this bizarre, easy-to-get-wrong 1900 offset —
// a frequent, very real source of off-by-1900 bugs in old codebases
// ── The "correct" pre-java.time way to extract fields required Calendar
Calendar cal = Calendar.getInstance();
cal.setTime(now);
int year = cal.get(Calendar.YEAR); // the actual, non-offset year — but via a SEPARATE classMutability and SimpleDateFormat's Thread-Safety Hazard
Unlike every type in java.time, java.util.Date is mutable — setTime(long) and the deprecated field-setting methods (setYear, setMonth, and so on) modify the existing Date object in place rather than returning a new one, meaning a Date reference handed to another piece of code (stored in a shared field, passed into a method, returned from a getter) can be silently changed by that other code later, with the original holder of the reference having no way to detect or prevent it unless it had defensively copied the Date first. This is a fundamentally different and more dangerous design than java.time's immutability, and was a real, recurring source of bugs in pre-Java-8 codebases — a Date returned from a getter and then mutated by calling code unexpectedly corrupting the internal state of the object that originally owned it, since both references pointed at the very same mutable object the whole time.
SimpleDateFormat, the legacy formatting/parsing counterpart to DateTimeFormatter, compounds this with its own, separate thread-safety hazard: it is internally mutable and explicitly documented as not synchronized, meaning a single SimpleDateFormat instance shared across multiple threads without external synchronization can have its format()/parse() calls interleave and corrupt each other's intermediate state, producing wrong output (or throwing unexpected exceptions) under concurrent use — a famous and frequently-rediscovered pitfall that led to the common, defensive (but easy to forget) practice of creating a brand-new SimpleDateFormat per use, or wrapping access to a shared one in a ThreadLocal, purely to sidestep the underlying class's lack of thread-safety.
Java
// ── Date is MUTABLE — a shared reference can be changed out from under you
public class Appointment {
private Date when;
public Date getWhen() { return when; } // returns the LIVE, mutable object directly!
}
Appointment appt = new Appointment();
Date exposedRef = appt.getWhen();
exposedRef.setTime(0L); // ← THIS SILENTLY CORRUPTS appt's internal state too —
// both references point at the exact same mutable Date object
// ── SimpleDateFormat — NOT thread-safe, sharing across threads is a real bug
private static final SimpleDateFormat SHARED_FORMAT =
new SimpleDateFormat("yyyy-MM-dd"); // DANGEROUS if multiple threads call it concurrently
// Two threads calling SHARED_FORMAT.format(...) or .parse(...) at the same time
// can corrupt each other's internal Calendar state, producing wrong dates silently
// ── The defensive (but easy to forget) old workaround ──────────────────
private static final ThreadLocal<SimpleDateFormat> THREAD_SAFE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));Interoperating With java.time
Because a large amount of existing code, libraries, and APIs (including parts of the JDK itself, such as some legacy I/O and networking classes) still produce or expect java.util.Date, java.time provides direct, explicit conversion methods specifically for bridging between the two worlds rather than forcing a full migration before any java.time code can be used at all. Date.toInstant() converts a legacy Date into a java.time Instant, capturing the equivalent point on the universal timeline with no loss of the date's underlying millisecond-precision instant data, and Date.from(Instant) performs the reverse conversion, constructing a new legacy Date from an existing Instant — both of these methods were added to java.util.Date specifically in Java 8, purely to support this transition, despite Date itself not being deprecated or otherwise modified by the java.time redesign.
From the Instant obtained via toInstant(), the rest of java.time's richer API becomes available by further conversion — instant.atZone(ZoneId) produces a full ZonedDateTime with human-readable local fields, exactly as covered earlier — meaning the typical, recommended migration pattern for code straddling both APIs is to convert any incoming legacy Date to an Instant (and onward to ZonedDateTime or LocalDateTime as needed) as early as possible, do all actual date/time logic using java.time types exclusively, and only convert back to a legacy Date at the boundary, immediately before handing a value off to whatever legacy API still requires it.
Java
// ── Legacy Date → java.time, via Instant as the bridge ────────────────
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant(); // capture the SAME instant, java.time-side
ZonedDateTime zoned = instant.atZone(ZoneId.of("Asia/Tokyo")); // full java.time API from here
System.out.println(zoned); // human-readable, zone-aware — do all real logic from this point on
// ── java.time → legacy Date, at the boundary, right before a legacy API call
Instant resultInstant = Instant.now();
Date forLegacyApi = Date.from(resultInstant); // convert back ONLY when a legacy API demands it
legacyApiThatStillTakesDate(forLegacyApi); // some old method signature requiring java.util.Date
// ── Recommended pattern: convert at the boundary, do all logic in java.time
void processIncoming(Date incoming) {
LocalDateTime working = incoming.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime(); // ← all subsequent logic uses THIS, never the raw Date again
}Related Topics in Date Time API
LocalDate
LocalDate is the java.time class (introduced in Java 8 as part of the redesigned date-time API replacing the deprecated java.util.Date/Calendar) representing a date without a time component or a time zone — just a year, month, and day on the ISO-8601 calendar. This entry covers why LocalDate deliberately carries no time-of-day or zone information, how it is created and parsed, how it is manipulated via its immutable, fluent with*/plus*/minus* methods, how two LocalDate instances are compared and the period between them measured, and the common pitfalls of treating it as if it secretly carried a time or zone after all.
LocalTime
LocalTime is the java.time class representing a time-of-day without any date or time zone attached — just an hour, minute, second, and nanosecond on a 24-hour clock. This entry covers what LocalTime intentionally omits and why, how to create and parse LocalTime values with varying precision, how arithmetic on LocalTime wraps around midnight instead of rolling into a different day, and how LocalTime is meant to be combined with LocalDate (via LocalDateTime) rather than used as a date substitute.
LocalDateTime
LocalDateTime is the java.time class combining a LocalDate and a LocalTime into a single date-and-time-of-day value that still, deliberately, carries no time zone or UTC-offset information. This entry covers why LocalDateTime is best understood as a composite of the two simpler types rather than a primitive type in its own right, how it is created from and decomposed back into its LocalDate/LocalTime parts, why the absence of a zone makes it unsuitable for representing one unambiguous, real-world instant, and how it relates to and converts into the zone-aware types (ZonedDateTime, Instant) when an actual moment in time is what's needed.
ZonedDateTime
ZonedDateTime is the java.time class representing a LocalDateTime combined with a ZoneId, fully resolved against the time zone's rules to also carry a specific UTC offset — making it the type capable of representing one genuinely unambiguous real-world instant while still preserving human-readable, zone-local wall-clock fields. This entry covers the relationship between ZoneId and the resolved offset, how daylight saving transitions create gaps and overlaps that ZonedDateTime must resolve, how zone-aware arithmetic differs from plain LocalDateTime arithmetic across those transitions, and when to prefer ZonedDateTime over the simpler offset-only OffsetDateTime or the zone-independent Instant.