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 epochnew 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()returnsyear - 1900. For 2025, it returns125. 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 writegetMonth() + 1and forget it once.- Every accessor reads in the JVM default time zone. Same
Dateon two machines in different zones gives differentgetDate()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 8toInstant() 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 goneFor 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— likeDatebut 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 placeThat 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.
What to take from the run:
Date.toString()printed in the JVM's default time zone. The sameDatevalue displays differently on a UTC server vs. an America/New_York laptop. That's the central design flaw that drove thejava.timeredesign — 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 toZonedDateTimeand supply the zone explicitly.now.getYear()returned125for 2025. Theyear - 1900convention was a Java 1.0 mistake never fixed; the method was deprecated in 1.1 and is still there. AnygetYear()call on aDatereading "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 throughDate.fromand 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 throughDate.- Legacy +1 day was
now.getTime() + 24L * 60 * 60 * 1000— manual arithmetic, easy to typo (forget theLand overflow). Moderninst.plus(Duration.ofDays(1))is type-safe and reads aloud. When you're migrating, replacing everytime + N * mscalculation withDurationis 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
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?