W3docs

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-dd

now() 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 January

If 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 366

Month 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 risk

Each 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 year

Every 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 / +1

LocalDate 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 month

The 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.

java— editable, runs on the server

What to take from the run:

  • LocalDate.of(2025, Month.NOVEMBER, 4) was the safe form. The integer overload (2025, 11, 4) works, but the Month enum makes it impossible to use 0 for January — the legacy mistake of GregorianCalendar. When the second argument can be either, use the enum.
  • plusDays(30) returned a fresh LocalDate; printing the original at the end of the program showed it untouched. Every arithmetic and with* method follows this rule, which is what makes the type thread-safe by construction. No defensive copying needed; passing a LocalDate to 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, but jan31.plusMonths(1).minusMonths(1) returns January 28, not January 31. Round-trip-and-expect-the-original works for plusDays/minusDays, not for plusMonths/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 signed long. The companion invoiceDate.until(today) returned a Period — calendar-shaped, with separate year/month/day fields. The two answer different questions: ChronoUnit.X for "how many whole X," Period for "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

Practice

`LocalDate.of(2025, 1, 31).plusMonths(1)` returns what value, and why?