W3docs

Java Legacy Date Class

The legacy java.util.Date class — why it's superseded by java.time, and how to convert between them.

Java Legacy Date Class

java.util.Date was Java's original date-time class, present since Java 1.0 in 1995. It is still in the JDK; new code shouldn't use it, but you'll encounter it in libraries, databases (java.sql.Date extends it), and any code older than about 2014. This chapter exists to bridge it to java.time.

The short version: Date is internally a wrapper around an epoch-millisecond long, much like Instant is internally a (seconds, nanos) pair. So the natural conversion is Date ↔ Instant. For anything else (year, month, day, calendar arithmetic), convert to java.time first.

What Date actually is

public class Date implements Cloneable, Comparable<Date>, Serializable {
  private long fastTime;        // milliseconds since 1970-01-01T00:00:00Z
}

That's the whole real state. A Date is a point in time, measured in milliseconds since the Unix epoch, in UTC. Despite the name, it doesn't carry a calendar date — getYear() and friends compute from the millisecond value at the JVM default time zone, which is the source of the API's famous problems.

Date now = new Date();                                       // current moment
Date epoch = new Date(0);                                    // 1970-01-01T00:00:00Z
Date fromMs = new Date(1_700_000_000_000L);

long ms = now.getTime();                                     // milliseconds since epoch

new Date() and Date.getTime() are the two methods that have aged well. Everything else has either been deprecated or carries a footgun.

The deprecated calendar accessors

These methods were deprecated in Java 1.1 (1997) when Calendar was added:

date.getYear();                                              // year - 1900   (deprecated)
date.getMonth();                                             // 0-11          (deprecated)
date.getDate();                                              // 1-31, day of month (deprecated)
date.getDay();                                               // 0-6, day of week (deprecated)
date.getHours(); date.getMinutes(); date.getSeconds();       // local-zone reads (deprecated)

The deprecations have been there for 28 years. They still work. The footguns:

  • getYear() returns year - 1900. For 2025, it returns 125. This is a hands-down candidate for "weirdest API decision in the JDK."
  • getMonth() returns 0-11. January is 0, December is 11. Off-by-one bugs are guaranteed if you write getMonth() + 1 and forget it once.
  • Every accessor reads in the JVM default time zone. Same Date on two machines in different zones gives different getDate() results.

Don't call these. The minute you find yourself reaching for date.getYear(), convert to Instant/ZonedDateTime and use the modern accessors.

The Date ↔ Instant bridge

Date legacy = new Date();
Instant inst = legacy.toInstant();                            // since Java 8

Instant other = Instant.parse("2025-11-04T19:30:00Z");
Date back = Date.from(other);                                 // since Java 8

toInstant() and Date.from(...) are the modern conversion methods, added with java.time. They're the only two java.util.Date calls you should write in new code.

The conversion is lossy in one direction: Date is millisecond precision, Instant is nanosecond precision. Round-tripping Instant → Date → Instant truncates the sub-millisecond nanos:

Instant high = Instant.parse("2025-11-04T19:30:00.123456789Z");
Instant low = Date.from(high).toInstant();
// low = 2025-11-04T19:30:00.123Z — the 456789 nanos are gone

For server timestamps this is fine; for high-resolution event capture, stay in Instant end-to-end.

java.sql.Date and java.sql.Timestamp

The JDBC types are subclasses of java.util.Date:

  • java.sql.Date — a date with no time (the time component is forced to 00:00:00 in some zone). Misleading name; it's still a milliseconds-since-epoch wrapper underneath.
  • java.sql.Time — a time with no date.
  • java.sql.Timestamp — like Date but with nanosecond precision.

They all have JDK-8 conversion methods:

java.sql.Date    sqlDate  = java.sql.Date.valueOf(LocalDate.of(2025, 11, 4));
LocalDate localDate = sqlDate.toLocalDate();

java.sql.Timestamp ts = java.sql.Timestamp.from(Instant.now());
Instant inst = ts.toInstant();

Modern JDBC drivers also accept java.time types directly via setObject/getObject — for new code, skip the java.sql.* types and use LocalDate/Instant. The conversions are there for code that has to interoperate with a driver or framework that hasn't migrated.

Comparing and sorting

Date implements Comparable<Date>. The order is by milliseconds since epoch — same as Instant. So sorting List<Date> works the same as sorting List<Instant>.

equals compares the underlying long. Two Date values are equal iff they have the same millisecond value. Hashing works (it's (int)(time ^ (time >>> 32))), so Date is fine as a HashMap key — though again, Instant is the modern choice.

Mutability

The biggest hidden gotcha: Date is mutable.

Date d = new Date();
d.setTime(0);                                                // mutates d in place

That means any method that accepts or returns a Date is dangerous — the caller can change the value after they pass it; the callee can change the value the caller holds. Library code defensively copies (new Date(d.getTime())) every Date it receives. This is the kind of bookkeeping that java.time eliminated entirely by making every type immutable.

In legacy code, treat any Date field as if it could change underneath you. Convert to Instant at the API boundary if you need a stable snapshot.

SimpleDateFormat: the other deprecated thing

Paired with Date in old code is java.text.SimpleDateFormat:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String s = sdf.format(new Date());
Date d = sdf.parse("2025-11-04");

SimpleDateFormat is not thread-safe. Sharing one across threads will produce wrong output, exceptions, or both, intermittently. The standard workaround in legacy code is ThreadLocal<SimpleDateFormat>; the modern replacement is DateTimeFormatter, which is thread-safe and cacheable.

If you maintain code with a static SimpleDateFormat, treat it as a known bug regardless of whether anyone has reported a failure yet.

A worked example: legacy interop

The program below uses Date as you'd find it in a legacy API and shows the conversion path to java.time for every operation. Read it as "here's the migration recipe": every legacy call has a one-line Instant or ZonedDateTime equivalent.

java— editable, runs on the server

What to take from the run:

  • Date.toString() printed in the JVM's default time zone. The same Date value displays differently on a UTC server vs. an America/New_York laptop. That's the central design flaw that drove the java.time redesign — the value is UTC, the display is local, and the API gives you no easy way to tell which view you're looking at. If you care which zone you mean, convert to ZonedDateTime and supply the zone explicitly.
  • now.getYear() returned 125 for 2025. The year - 1900 convention was a Java 1.0 mistake never fixed; the method was deprecated in 1.1 and is still there. Any getYear() call on a Date reading "125" or "85" is a bug waiting to display itself to a user.
  • Instant.parse("...nanoseconds") printed nine digits of precision; the same value round-tripped through Date.from and back lost the last six. For server logs (millisecond precision is plenty), the truncation doesn't matter. For "I captured this event with high-precision timing," don't round-trip through Date.
  • Legacy +1 day was now.getTime() + 24L * 60 * 60 * 1000 — manual arithmetic, easy to typo (forget the L and overflow). Modern inst.plus(Duration.ofDays(1)) is type-safe and reads aloud. When you're migrating, replacing every time + N * ms calculation with Duration is the lowest-hanging fruit.
  • The mutability demo at the end showed shared.setTime(0) changing the value seen through both references. In a multi-threaded codebase that's a race condition; in single-threaded code it's still a defensive-copy ritual the JDK forced on every library. The modern API never asks for that ritual.

What's next

java.util.Date is one half of the legacy API. The other half is java.util.Calendar — the class added in Java 1.1 to give Date the calendar accessors Date itself shouldn't have. The next chapter, Java Calendar Class, is the last in this part and covers it with the same migration-recipe shape: every legacy call has a java.time replacement.

Practice

Practice

An old library returns `java.util.Date` from `getCreatedAt()`. You want to know 'is this in the same calendar day as today in New York?'. What's the right path?