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-8601The 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 expectedThat'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.
What to take from the run:
Duration.between(startShift, endShift)producedPT8H. The shift crossed midnight, and the date components carried the carry — there was no ambiguity. The same calculation on bareLocalTimes would have returnedPT-16H(the LocalTime gotcha from the previous chapter). For arithmetic that might cross midnight,LocalDateTimeis the type.startShift.plusHours(3)left the date at11-04(still that day);plusHours(5)rolled it forward to11-05T03:00. Theplus/minusfamily onLocalDateTimepropagates carries correctly through the wholeY/M/D/h/m/s/nschain. No special-casing required in your code.- The "next 09:30 meeting" block built today's 09:30 with three
withXcalls, then chose betweentodayandtomorrowbased onisBefore. 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 reasonLocalDateTimedoesn'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
nowunchanged afterplusDays(7).withHour(0).withMinute(0). That guarantee holds across every operation, every chain, every helper — there is no way to mutate aLocalDateTime. Pass it freely, share it across threads, store it in aMap.
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
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?