Convert LocalDateTime to Date in Java and Back

Convert LocalDateTime to Date in Java with atZone() and Date.from(), and back with ofInstant(), including DST gaps, overlaps and lost nanoseconds.

Conversion path between LocalDateTime and java.util.Date through ZonedDateTime and Instant, with the daylight saving gap and overlap cases

We convert a java.util.Date to a LocalDateTime with LocalDateTime.ofInstant(date.toInstant(), zone), and a LocalDateTime back to a Date with Date.from(localDateTime.atZone(zone).toInstant()). The ZoneId decides which wall-clock time the moment stored in the Date becomes, and back.

The two conversion calls belong where our code meets an API that still uses Date, such as a job scheduler, an old SOAP client or a reporting library. The rest of the code keeps LocalDateTime, ZonedDateTime or Instant values and never passes a Date around.

The following example takes the next run time of a scheduled job, which the scheduler returns as a Date, moves it by two hours in Berlin time and converts it back.

Date nextRun = Date.from(Instant.parse("2026-10-12T05:00:00Z"));
ZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime local = LocalDateTime.ofInstant(nextRun.toInstant(), berlin);   // 2026-10-12T07:00
LocalDateTime moved = local.plusHours(2);                                     // 2026-10-12T09:00
Date forScheduler = Date.from(moved.atZone(berlin).toInstant());
Instant check = forScheduler.toInstant();                                     // 2026-10-12T07:00:00Z

We can see that 05:00 UTC is 07:00 in Berlin in October, so the zone shifts the clock time by two hours. The sections cover what each class stores, both directions, the daylight saving cases that change a time on the way back, precision loss, and a scheduler example.

1. A Moment Versus a Wall-Clock Time

A java.util.Date holds a moment, stored as milliseconds since 1970-01-01T00:00:00Z. A LocalDateTime holds what a wall clock shows, such as 2026-10-12T07:00, with no zone, so it is not a moment until we say which zone the clock belongs to.

java.util.DateLocalDateTime
StoresMilliseconds since the epoch (UTC)Year, month, day, hour, minute, second, nanosecond
Time zoneNone, toString() uses the JVM default zoneNone
PrecisionMillisecondsNanoseconds
MutableYes, setTime() changes itNo, every change returns a new object
Closest java.time typeInstantNot a moment, so no direct match

The last row explains the conversion path. A Date maps to an Instant one to one, and the Instant plus a ZoneId gives a ZonedDateTime, whose local part is the LocalDateTime.

Conversion path between LocalDateTime and java.util.Date through ZonedDateTime and Instant, with the daylight saving gap and overlap cases
Every conversion passes through a zone; a wall-clock time inside a DST gap moves forward, and one inside an overlap takes the earlier offset

2. From Date to LocalDateTime

The factory method LocalDateTime.ofInstant(Instant, ZoneId) is the shortest way, and it exists since Java 8. The longer form attaches the zone with atZone() and calls toLocalDateTime(), which gives the same value and is useful when we also need the ZonedDateTime.

Date created = new Date(1791781200000L);
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
LocalDateTime viaFactory = LocalDateTime.ofInstant(created.toInstant(), tokyo);   // 2026-10-12T14:00
LocalDateTime viaZoned = created.toInstant().atZone(tokyo).toLocalDateTime();     // 2026-10-12T14:00
LocalDateTime inUtc = LocalDateTime.ofInstant(created.toInstant(), ZoneOffset.UTC);   // 2026-10-12T05:00

The same Date gives 14:00 in Tokyo and 05:00 in UTC. We pass the zone the user or the business works in, and avoid ZoneId.systemDefault() on servers, where the default zone is set by the machine and not by the business rule.

2.1. From a java.sql.Timestamp

A JDBC driver returns a java.sql.Timestamp for a TIMESTAMP column. It is a subclass of Date with nanosecond precision, and its toLocalDateTime() method returns the fields in the JVM default zone, which is how the driver created the object.

java.sql.Timestamp column = java.sql.Timestamp.valueOf(LocalDateTime.of(2026, 10, 12, 7, 0, 0, 123456789));
LocalDateTime fromColumn = column.toLocalDateTime();     // 2026-10-12T07:00:00.123456789
Date asDate = column;
long millis = asDate.getTime() % 1000;                   // 123

A Timestamp used as a plain Date exposes only milliseconds. With a JDBC 4.2 driver, we skip Timestamp and read the column with rs.getObject(“created_at”, LocalDateTime.class), as LocalDate to java.sql.Date shows for all java.sql types.

3. From LocalDateTime Back to Date

The way back needs the zone again. The call atZone(zone) turns the wall-clock time into a ZonedDateTime, toInstant() takes its moment, and Date.from() wraps that moment in a Date.

LocalDateTime meeting = LocalDateTime.of(2026, 10, 12, 9, 30);
Date inBerlin = Date.from(meeting.atZone(ZoneId.of("Europe/Berlin")).toInstant());
Date inNewYork = Date.from(meeting.atZone(ZoneId.of("America/New_York")).toInstant());
Instant berlinMoment = inBerlin.toInstant();             // 2026-10-12T07:30:00Z
Instant newYorkMoment = inNewYork.toInstant();           // 2026-10-12T13:30:00Z

One LocalDateTime gives two Date objects six hours apart, which is why a converter without a zone parameter hides a bug. LocalDateTime to ZonedDateTime covers the first step of the chain in more depth.

3.1. Clock Times That Do Not Exist or Happen Twice

A LocalDateTime that falls into a daylight saving gap does not exist in that zone, and atZone() moves it forward by the length of the gap without an exception. In Berlin, clocks jump from 02:00 to 03:00 on March 29, 2026, so 02:30 becomes 03:30.

ZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime inGap = LocalDateTime.of(2026, 3, 29, 2, 30);
ZonedDateTime moved = inGap.atZone(berlin);                       // 2026-03-29T03:30+02:00[Europe/Berlin]
LocalDateTime roundTrip = LocalDateTime.ofInstant(Date.from(moved.toInstant()).toInstant(), berlin);   // 2026-03-29T03:30

On October 25, 2026, Berlin clocks go from 03:00 back to 02:00, so 02:30 happens twice. The method atZone() picks the earlier offset, and withLaterOffsetAtOverlap() picks the second 02:30.

ZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime twice = LocalDateTime.of(2026, 10, 25, 2, 30);
Instant first = twice.atZone(berlin).toInstant();                              // 2026-10-25T00:30:00Z
Instant second = twice.atZone(berlin).withLaterOffsetAtOverlap().toInstant();  // 2026-10-25T01:30:00Z

For most apps the default is fine. Code that must not move a time, such as a payroll system that records shift ends, checks berlin.getRules().isValidOffset(time, offset) or stores the moment as an Instant from the start.

3.2. Nanoseconds Become Milliseconds

A LocalDateTime keeps nanoseconds, and a Date keeps milliseconds. The method Date.from() drops the extra digits, so a round trip loses them.

ZoneId utc = ZoneOffset.UTC;
LocalDateTime precise = LocalDateTime.of(2026, 10, 12, 7, 0, 0, 123456789);
Date truncated = Date.from(precise.atZone(utc).toInstant());
LocalDateTime back = LocalDateTime.ofInstant(truncated.toInstant(), utc);     // 2026-10-12T07:00:00.123
boolean same = back.equals(precise);                                          // false

Tests that compare a value before and after a Date round trip fail because of the lost digits. We truncate the original with truncatedTo(ChronoUnit.MILLIS) before comparing.

4. Starting a Quartz Job at a LocalDateTime

Say a reporting app sends a sales report at 07:00 Berlin time, and the scheduling uses Quartz, whose TriggerBuilder.startAt() takes a Date and whose triggers return Date values. The app keeps the report time as a LocalDateTime plus a zone and converts only at the Quartz call.

LocalDateTime reportTime = LocalDateTime.of(2026, 10, 12, 7, 0);
ZoneId berlin = ZoneId.of("Europe/Berlin");
Date startAt = Date.from(reportTime.atZone(berlin).toInstant());
Trigger trigger = TriggerBuilder.newTrigger().withIdentity("salesReport").startAt(startAt).build();
LocalDateTime scheduled = LocalDateTime.ofInstant(trigger.getStartTime().toInstant(), berlin);   // 2026-10-12T07:00

Reading the start time back through the same zone shows the 07:00 that the team configured. For a job that repeats daily, the Quartz method CronScheduleBuilder.inTimeZone() takes a TimeZone, which we get from TimeZone.getTimeZone(berlin), so the job follows Berlin daylight saving time and does not stay on a fixed UTC hour.

5. When the Value Is a Moment, Use Instant

Many Date fields record when something happened, such as a payment time or a log entry. A moment needs no wall-clock time, so the right replacement is an Instant, and Date.from(instant) and date.toInstant() convert it without a zone.

Instant paidAt = Instant.parse("2026-10-12T05:00:00Z");
Date legacyPaid = Date.from(paidAt);
Instant restored = legacyPaid.toInstant();                    // 2026-10-12T05:00:00Z
OffsetDateTime forApi = restored.atOffset(ZoneOffset.UTC);    // 2026-10-12T05:00Z

We convert to LocalDateTime only when we need the clock time in one known place, such as a time printed on a ticket. The conversion for whole days is in Date to LocalDate.

6. LocalDateTime and Date Conversion FAQs

Developers who mix the two classes run into the same few questions.

6.1. Why does the converted LocalDateTime show a different hour?

Because the zone used for the conversion differs from the zone we expected. A Date has no zone, and its toString() prints the JVM default zone, so the printed hour and the converted hour differ when the code passes another ZoneId.

6.2. How do we convert a LocalDateTime to a Date in UTC?

We use ZoneOffset.UTC, for example Date.from(ldt.toInstant(ZoneOffset.UTC)). The method toInstant(ZoneOffset) on LocalDateTime skips the ZonedDateTime step when the offset is fixed.

6.3. Is Timestamp.valueOf(localDateTime) a replacement for Date.from()?

Only for JDBC code. The result is a Timestamp in the JVM default zone, and timestamp.equals(date) returns false for a plain Date with the same time, whereas date.equals(timestamp) returns true. Mixing the two in collections or tests gives surprising results.

6.4. How do we convert Date to LocalDateTime on Java 8?

The same way as on Java 25. Unlike LocalDate.ofInstant(), which arrived in Java 9, LocalDateTime.ofInstant(date.toInstant(), zone) exists since Java 8, and date.toInstant().atZone(zone).toLocalDateTime() gives the same value, as section 2 shows.

7. Conclusion

A Date is a moment and a LocalDateTime is a wall-clock time, so every conversion between them goes through a ZoneId. We use LocalDateTime.ofInstant(date.toInstant(), zone) in one direction and Date.from(ldt.atZone(zone).toInstant()) in the other.

The way back can move a time that falls into a daylight saving gap and picks the earlier offset in an overlap, and it always drops digits below milliseconds. We keep these conversions at the edge of the code, next to the old API, and use Instant where the value is a moment. The Java date and time tutorials cover each java.time class.

8. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.