Java LocalDate
Represent dates without time or time zone in Java with LocalDate — creation, manipulation, and queries.
Java LocalDate
LocalDate is a calendar date — year, month, day — with no time of day and no time zone. It represents the same date on every clock everywhere: when you write LocalDate.of(2025, 11, 4), that's the fourth of November in the ISO calendar, full stop. No 14:30 attached, no UTC offset, no Tokyo-vs-Honolulu ambiguity.
That makes it the right type for a lot of things that the legacy java.util.Date mishandled: birthdays, contract dates, invoice dates, the date a UI date-picker selected. Anywhere a calendar day is the unit, LocalDate is the class.
Creating
The three standard factories:
LocalDate today = LocalDate.now(); // system default zone
LocalDate stardate = LocalDate.of(2025, 11, 4); // year, month (1-12), day (1-31)
LocalDate parsed = LocalDate.parse("2025-11-04"); // ISO-8601 yyyy-MM-ddnow() reads the current date in the JVM's default time zone. That's almost always what you want; for tests it's a problem, and the Clock-overloaded forms (LocalDate.now(clock)) let you inject a fixed clock. The parsing chapter covers parse with custom formats; the default accepts ISO-8601 dates only.
You can also use a Month enum instead of a 1..12 integer:
LocalDate.of(2025, Month.NOVEMBER, 4); // type-safe; no risk of using 0 for JanuaryIf you've ever written new GregorianCalendar(2025, 11, 4) and gotten December (because the legacy API uses zero-based months), the enum form is the upgrade you want.
Inspecting
The accessor catalogue:
int year = date.getYear();
Month month = date.getMonth(); // enum
int monthVal = date.getMonthValue(); // 1-12
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek(); // enum: MONDAY, TUESDAY, ...
int dayOfYear = date.getDayOfYear(); // 1-366
boolean leap = date.isLeapYear();
int monthLen = date.lengthOfMonth(); // 28-31
int yearLen = date.lengthOfYear(); // 365 or 366Month and DayOfWeek are enums. Use them; they make code that compares to a specific day or month dramatically clearer:
if (date.getDayOfWeek() == DayOfWeek.MONDAY) ... // type-safe
if (date.getMonth() == Month.NOVEMBER) ... // no off-by-one riskEach enum has helper methods of its own — Month.length(boolean leap), DayOfWeek.getValue() returning 1-7 with Monday = 1, and DayOfWeek.plus(7) for "the same day, n days from now."
Modifying — every method returns a new instance
The arithmetic methods:
date.plusDays(7); // a week later
date.plusWeeks(2);
date.plusMonths(1); // careful: month length varies
date.plusYears(1);
date.minusDays(30);
date.minusYears(5);And the "replace one field" forms:
date.withYear(2026);
date.withMonth(1);
date.withDayOfMonth(1);
date.withDayOfYear(1); // first day of the yearEvery one of these returns a new LocalDate. The original is unchanged. date.plusDays(7) and forgetting to capture the result is a no-op — and a bug we have all written at least once.
The "month length varies" caveat for plusMonths: when adding a month would land on a day that doesn't exist in the target month, java.time clamps to the last day. LocalDate.of(2025, 1, 31).plusMonths(1) is 2025-02-28 (or 02-29 in a leap year), not 2025-03-03. The behaviour is documented and consistent, but it does mean plusMonths(1) and minusMonths(1) are not always inverses.
Comparing
date.isBefore(other);
date.isAfter(other);
date.isEqual(other); // same as equals here; useful on ZonedDateTime
date.compareTo(other); // -1 / 0 / +1LocalDate implements Comparable<LocalDate>, so it sorts naturally in any collection. For "is this date in [start, end]?" the typical shape is !date.isBefore(start) && !date.isAfter(end).
Distance: until and ChronoUnit.between
How many days between two dates?
long days = ChronoUnit.DAYS.between(start, end); // a long; signed
long weeks = ChronoUnit.WEEKS.between(start, end);
long months = ChronoUnit.MONTHS.between(start, end);
Period diff = start.until(end); // a Period (years/months/days)ChronoUnit.X.between is the right call for "how many whole X are between these?" until returns a Period, which is the calendar-shaped breakdown — useful for "you've been a member for 2 years, 3 months, 14 days."
Note the sign convention: between(start, end) is positive when end is after start, negative otherwise.
The "what day of the week is..." shortcut
The temporal-adjusters package gives you the predicates you'd otherwise compute by hand:
import static java.time.temporal.TemporalAdjusters.*;
date.with(firstDayOfMonth());
date.with(lastDayOfMonth());
date.with(firstDayOfNextMonth());
date.with(next(DayOfWeek.MONDAY)); // next Monday strictly after `date`
date.with(nextOrSame(DayOfWeek.MONDAY)); // today if today is Monday, else next
date.with(previousOrSame(DayOfWeek.SUNDAY));
date.with(lastInMonth(DayOfWeek.FRIDAY)); // last Friday of the monthThe Temporal Adjusters chapter covers these in depth. For now, the takeaway: don't write "next Monday after this date" by hand; the adjusters already have it.
Time-zone caveat
LocalDate has no zone, so LocalDate.now() has to pick one to know which calendar day "now" is. The default is the JVM's default zone (ZoneId.systemDefault()). If you're running on a server set to UTC at 23:30 local time in New York, LocalDate.now() returns tomorrow's date from a New York perspective — because the JVM's zone says it's already past midnight UTC.
For a date that's local to a known zone, pass the zone explicitly:
LocalDate tokyoToday = LocalDate.now(ZoneId.of("Asia/Tokyo"));This bites in production exactly when the developer's laptop is in a different zone from the deployed server. Be explicit when the zone matters.
A worked example: invoice-date arithmetic
The program below uses LocalDate for the kind of work a small invoicing system would do — generate an invoice date, compute due dates, count days outstanding, figure out month-end, and find the next business day. It's the realistic shape of LocalDate code.
What to take from the run:
LocalDate.of(2025, Month.NOVEMBER, 4)was the safe form. The integer overload (2025, 11, 4) works, but theMonthenum makes it impossible to use 0 for January — the legacy mistake ofGregorianCalendar. When the second argument can be either, use the enum.plusDays(30)returned a freshLocalDate; printing the original at the end of the program showed it untouched. Every arithmetic andwith*method follows this rule, which is what makes the type thread-safe by construction. No defensive copying needed; passing aLocalDateto a method is always safe.- The
plusMonths(1)demo showed the clamp behaviour: January 31 + 1 month = February 28 (or 29 in a leap year). The behaviour is documented and consistent, butjan31.plusMonths(1).minusMonths(1)returnsJanuary 28, notJanuary 31. Round-trip-and-expect-the-original works forplusDays/minusDays, not forplusMonths/minusMonths. - The temporal adjusters (
lastDayOfMonth,firstDayOfNextMonth,nextOrSame(MONDAY)) replaced multi-line by-hand calendar walks. Chained, they express "the first Monday on or after the first of next month" in two adjusters. The next chapter on LocalTime and the dedicated Temporal Adjusters chapter go deeper. ChronoUnit.DAYS.between(invoice, today)returned a signedlong. The companioninvoiceDate.until(today)returned aPeriod— calendar-shaped, with separate year/month/day fields. The two answer different questions:ChronoUnit.Xfor "how many whole X,"Periodfor "in calendar-friendly form." Pick the one whose shape matches the output you want.
What's next
LocalDate was the date side. The next chapter, Java LocalTime, is its mirror — time of day, with no date attached and no zone. Same fluent API, smaller class, same immutability guarantees.
Practice
`LocalDate.of(2025, 1, 31).plusMonths(1)` returns what value, and why?