W3docs

Java LocalDateTime

Combine date and time without zone information in Java with LocalDateTime.

Java LocalDateTime

LocalDateTime is LocalDate and LocalTime glued together — a calendar date and a time of day, with still no time zone. It's the natural value for "this event happened at 14:30 on November 4," and it's the most-used java.time type in real codebases that don't deal with global users.

The fluent API is identical in shape to the two halves — same now/of/parse factories, same plusX/minusX/withX modifiers, same isBefore/isAfter comparisons. The interesting parts are the new ones: combining with a date or time, decomposing back, and the explicit conversion to ZonedDateTime when the zone finally matters.

Creating

LocalDateTime now    = LocalDateTime.now();                  // JVM default zone
LocalDateTime made   = LocalDateTime.of(2025, 11, 4, 14, 30);
LocalDateTime made2  = LocalDateTime.of(2025, Month.NOVEMBER, 4, 14, 30, 15);
LocalDateTime made3  = LocalDateTime.of(LocalDate.of(2025, 11, 4), LocalTime.of(14, 30));
LocalDateTime parsed = LocalDateTime.parse("2025-11-04T14:30:00");   // ISO-8601

The ISO-8601 form has a literal T between the date and time — that's the standard separator. LocalDateTime.parse("2025-11-04 14:30:00") (space instead of T) does not parse with the default; you'd need a custom DateTimeFormatter for that.

Decomposition

LocalDate date = dt.toLocalDate();
LocalTime time = dt.toLocalTime();

These are the inverse of LocalDate.atTime(time) / LocalTime.atDate(date). Both are pure projection — no information loss, no zone introduced.

All the accessors at once

LocalDateTime inherits the accessor menu from both halves:

dt.getYear();          dt.getMonth();        dt.getMonthValue();
dt.getDayOfMonth();    dt.getDayOfWeek();    dt.getDayOfYear();
dt.getHour();          dt.getMinute();       dt.getSecond();          dt.getNano();

Same names, same semantics. You don't have to call toLocalDate() or toLocalTime() to get at the individual pieces — they're all available directly.

Arithmetic across the boundary

The crucial difference from LocalTime: LocalDateTime.plusHours(3) on 23:00 does not wrap silently. It rolls into the next day:

LocalDateTime late = LocalDateTime.of(2025, 11, 4, 23, 0);
late.plusHours(3);   // 2025-11-05T02:00 — date advanced as expected

That's the whole reason for using LocalDateTime over LocalTime for any computation that might cross midnight. The math is consistent with what you'd expect from a real clock that knows what day it is.

dt.plusDays(7);          dt.plusHours(36);        dt.plusMinutes(150);
dt.minusYears(1);        dt.minusSeconds(45);

dt.withYear(2026);       dt.withHour(0);          dt.withMinute(0);

The plusMonths clamping rule from LocalDate applies here too: LocalDateTime.of(2025, 1, 31, 12, 0).plusMonths(1) is 2025-02-28T12:00, not 2025-03-03T12:00. The clamp is on the date component only; the time is unchanged.

The zone is missing on purpose

A LocalDateTime is not a moment on the global timeline. LocalDateTime.of(2025, 11, 4, 9, 0) could be 9 AM in New York, 9 AM in Berlin, or 9 AM in Tokyo — three very different Instants, and LocalDateTime won't tell you which. If two LocalDateTimes are equal, that means the date and time strings are equal; it does not mean the underlying moments are equal.

That's a feature, not a bug. For "the contract is signed at 14:00 local time wherever the signer is," LocalDateTime is exactly the right shape. For "the server received the request at...", it's the wrong shape — use Instant. For "the meeting starts at 14:00 New York time," wrong again — use ZonedDateTime.

To convert to a zoned moment, you have to add the zone explicitly:

ZonedDateTime ny = ldt.atZone(ZoneId.of("America/New_York"));
Instant       inst = ldt.atZone(ZoneId.systemDefault()).toInstant();

The atZone(...) is the load-bearing call — it's the moment the type system makes you decide which zone you mean. Once you've decided, the conversion to Instant is mechanical. The next two chapters (ZonedDateTime, Instant) cover the zoned and global shapes in detail.

Comparing

dt.isBefore(other);
dt.isAfter(other);
dt.isEqual(other);
dt.compareTo(other);

Ordering is lexicographic by (date, time). The same warning as before: two LocalDateTimes compare by their string representations of date and time, not by the underlying moments — because there are no underlying moments without a zone.

Distance

ChronoUnit.X.between works directly:

long minutes = ChronoUnit.MINUTES.between(start, end);
long days = ChronoUnit.DAYS.between(start, end);
Duration d = Duration.between(start, end);

Duration.between works on LocalDateTime (it works on any Temporal). For purely-calendar arithmetic — "how many months between these two LocalDateTimes" — use ChronoUnit.MONTHS.between, which gives a long, or Period.between(start.toLocalDate(), end.toLocalDate()) for the calendar-shaped breakdown.

A worked example: scheduling across midnight

The program below uses LocalDateTime for a small piece of scheduling code: a night-shift that starts at 22:00 and ends at 06:00, computing duration correctly across midnight; rounding "now" up to the next quarter hour; finding the next occurrence of a recurring 09:30 meeting; and demonstrating the zone-must-be-added-explicitly rule when converting to a moment in time.

java— editable, runs on the server

What to take from the run:

  • Duration.between(startShift, endShift) produced PT8H. The shift crossed midnight, and the date components carried the carry — there was no ambiguity. The same calculation on bare LocalTimes would have returned PT-16H (the LocalTime gotcha from the previous chapter). For arithmetic that might cross midnight, LocalDateTime is the type.
  • startShift.plusHours(3) left the date at 11-04 (still that day); plusHours(5) rolled it forward to 11-05T03:00. The plus/minus family on LocalDateTime propagates carries correctly through the whole Y/M/D/h/m/s/ns chain. No special-casing required in your code.
  • The "next 09:30 meeting" block built today's 09:30 with three withX calls, then chose between today and tomorrow based on isBefore. This is the common shape for "the next recurring event of this time-of-day" — small enough to inline, common enough to factor into a helper if you have many of them.
  • The "same LocalDateTime, different zones" block produced two different Instants six hours apart. That's the central reason LocalDateTime doesn't claim to be a moment. The class refuses to pretend a date-and-time alone is enough information; it's a label on a wall clock somewhere, and which wall depends on the zone you supply.
  • The closing immutability check showed now unchanged after plusDays(7).withHour(0).withMinute(0). That guarantee holds across every operation, every chain, every helper — there is no way to mutate a LocalDateTime. Pass it freely, share it across threads, store it in a Map.

What's next

LocalDateTime is the last of the three "Local" types — no zone, no claim to be a moment. The next chapter, Java ZonedDateTime, adds the zone explicitly: a LocalDateTime plus a ZoneId plus the resolved offset for that local time in that zone, which together pin down an actual moment on the global timeline.

Practice

Practice

Two developers — one in New York, one in Berlin — both build the value `LocalDateTime.of(2025, 11, 4, 14, 0)`. Are these the same moment in time?