🧰 Core APIs Toolbox · Intermediate

java.time: LocalDate & LocalDateTime

Immutable date/time without zone; plusDays returns a new object.

🧩 The mysteryJanuary 31st plus one month = …February 31st? March 3rd? An exception? Your billing system needs the answer before the invoices go out.

Calendar and clock

**LocalDate: a date like a birthday (2024-07-04). LocalTime: a wall-clock time like 09:00. LocalDateTime: both together. None of them has a time zone — they describe what's written on a calendar or clock. Months are 1–12** (the old Calendar used 0–11!).

Immutable objects

Every java.time object is immutable. plusDays, plusWeeks, withYear… return a new object; the original never changes.

LocalDate due = LocalDate.of(2024, 5, 1);
LocalDate later = due.plusWeeks(2);
LocalDateTime meet = due.atTime(14, 30);
// due is still 2024-05-01
🔮 Predict it

Lost days

What does this print?

LocalDate d = LocalDate.of(2025, 12, 30);
d.plusDays(5);
System.out.println(d);
System.out.println(d.plusDays(5));
  1. 2026-01-04 2026-01-04
  2. 2025-12-30 2026-01-04
  3. 2025-12-35 2026-01-04
Show the answer

Line 2 creates a new date and throws it away, so d is still 2025-12-30. Using the result gives 2026-01-04 — year rollover handled for you.

🤔 Think first

January 31 + 1 month?

What is LocalDate.of(2024, 1, 31).plusMonths(1)?

Think about it, then reveal the answer

2024-02-29. February 31 doesn't exist, so java.time clamps to the last valid day of the month. 2024 is a leap year, hence the 29th (in 2023 it would be the 28th).

⚠️ The trap

Invalid dates throw

LocalDate.of validates every field. February 29 in a non-leap year throws **DateTimeException** instead of silently rolling over to March 1 like the old lenient Calendar did.

LocalDate.of(2023, 2, 29);
// DateTimeException: 2023 isn't a leap year
LocalDate.of(2024, 2, 29); // fine
🔮 Predict it

Leap day birthday

What does this print?

LocalDate leap = LocalDate.of(2024, 2, 29);
System.out.println(leap.plusYears(1));
  1. 2025-03-01
  2. 2025-02-28
  3. Throws DateTimeException
Show the answer

2025-02-28 — another clamp: 2025 has no February 29, so java.time uses the last valid day of that month.

💼 In the real world

Dates in real apps

Subscription renewals, due dates and birthdays are classic LocalDate jobs. Month-end clamping is a real product decision: a plan bought on Jan 31 renews on Feb 29 — then on Mar 29 if you keep chaining from the last date, which is why billing systems usually compute from the *original* date.

Key takeaways

  1. Months are 1-12 (unlike old Calendar's 0-11)
  2. plusDays/withYear… return NEW objects
  3. Invalid dates throw DateTimeException
  4. Jan 31 + 1 month → last day of February
🤯 Did you know?

In the old API, Calendar.JANUARY is 0 — off-by-one month bugs were so common that java.time made months 1–12.

Practice questions

What does this print?

LocalDate d = LocalDate.of(2024, 3, 10);
d.plusDays(5);
System.out.println(d);
  1. 2024-03-15
  2. 2024-03-10
  3. 2024-08-10
  4. 2024-3-10
Check your answer

2024-03-10. plusDays returns a new LocalDate, but the result is ignored. d still holds 2024-03-10.

What does this print?

LocalDate d = LocalDate.of(2024, 1, 31)
    .plusMonths(1);
System.out.println(d);
  1. 2024-02-31
  2. 2024-03-02
  3. 2024-02-29
  4. Throws DateTimeException
Check your answer

2024-02-29. February 31 doesn't exist, so plusMonths clamps to the last valid day. 2024 is a leap year, so that's the 29th.

Next: Local types don't know *where* they are. Enter Instant and time zones — where 02:30 can simply not exist.