Java LocalDate Class
Java LocalDate Class
LocalDate represents a date without a time or a time zone attached to it — just a year, a month, and a day, nothing else. It is the class to reach for whenever the thing being modeled genuinely has no time component: a birthday, a subscription renewal date, an order's expected delivery date. Introduced in Java 8 as part of java.time, LocalDate is immutable and thread-safe, and every method that looks like it changes a date actually returns a new one.
What Is LocalDate?
java.time.LocalDate represents an ISO-8601 calendar date — year, month, and day — with no time-of-day and no time zone stored anywhere inside it. Internally it holds a year, a month numbered 1 through 12, and a day of the month, and it implements Comparable<LocalDate>, so dates sort in chronological order without needing a custom comparator. Its default toString() produces the ISO-8601 format yyyy-MM-dd, which is also exactly the format LocalDate.parse() expects back.
One sentence before the diagram: a LocalDate holds exactly three fields, nothing else, and every method that appears to change one actually returns a brand new object with the updated fields.
LocalDate.of(2026, 8, 25)
|
v
+------+-------+-------+
| year | month | day |
| 2026 | 8 | 25 |
+------+-------+-------+
|
v
date.plusDays(7) --> a NEW LocalDate(2026, 9, 1) is returned,
the original 2026-08-25 object is untouched
Every field on a LocalDate is set once at construction and never changes afterward — plusDays(), withDayOfMonth(), and every similar method return a new instance rather than mutating the one they were called on.
Why LocalDate Was Introduced
Before Java 8, representing "just a date" still meant reaching for java.util.Date, a class that actually stores a full timestamp down to the millisecond — a supposedly date-only value silently carried a hidden time component that nothing in the type system prevented later code from reading or comparing.
1// File: BeforeLocalDate.java
2import java.util.*;
3import java.text.*;
4
5public class BeforeLocalDate {
6 public static void main(String[] args) throws ParseException {
7 // A java.util.Date meant to represent "just a date" still carries
8 // hours, minutes, seconds, and milliseconds internally
9 SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
10 Date deliveryDate = parser.parse("2026-08-25");
11
12 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
13 System.out.println(formatter.format(deliveryDate));
14 }
15}Output:
2026-08-25 00:00:00
That hidden 00:00:00 was always there, waiting for some later piece of code to read it, compare it, or serialize it by accident. LocalDate removes the possibility entirely — there is no time component to hide, because the class structurally cannot represent one.
1// File: AfterLocalDate.java
2import java.time.*;
3
4public class AfterLocalDate {
5 public static void main(String[] args) {
6 LocalDate deliveryDate = LocalDate.parse("2026-08-25");
7
8 System.out.println(deliveryDate);
9 }
10}Output:
2026-08-25
Both versions represent the same calendar date. Only the second one guarantees, by its type alone, that nothing downstream can ever accidentally introduce a time-of-day bug.
Syntax
Creating a LocalDate, reading its fields, and adjusting it all follow a small, consistent method vocabulary.
1// File: LocalDateSyntaxForms.java
2import java.time.*;
3
4public class LocalDateSyntaxForms {
5 public static void main(String[] args) {
6 LocalDate fixedDate = LocalDate.of(2026, 8, 25);
7 LocalDate parsedDate = LocalDate.parse("2026-12-25");
8
9 System.out.println("of(): " + fixedDate);
10 System.out.println("parse(): " + parsedDate);
11
12 System.out.println("Year: " + fixedDate.getYear());
13 System.out.println("Month: " + fixedDate.getMonth());
14 System.out.println("Day of month: " + fixedDate.getDayOfMonth());
15 System.out.println("Day of week: " + fixedDate.getDayOfWeek());
16 System.out.println("Day of year: " + fixedDate.getDayOfYear());
17 System.out.println("Length of month: " + fixedDate.lengthOfMonth());
18 System.out.println("Is 2026 a leap year: " + fixedDate.isLeapYear());
19
20 System.out.println("plusDays(7): " + fixedDate.plusDays(7));
21 System.out.println("plusMonths(2): " + fixedDate.plusMonths(2));
22 System.out.println("minusYears(1): " + fixedDate.minusYears(1));
23 System.out.println("withDayOfMonth(1): " + fixedDate.withDayOfMonth(1));
24 }
25}Output:
of(): 2026-08-25
parse(): 2026-12-25
Year: 2026
Month: AUGUST
Day of month: 25
Day of week: TUESDAY
Day of year: 237
Length of month: 31
Is 2026 a leap year: false
plusDays(7): 2026-09-01
plusMonths(2): 2026-10-25
minusYears(1): 2025-08-25
withDayOfMonth(1): 2026-08-01
Common Use Cases
Checking Whether a Date Falls on a Weekend
getDayOfWeek() returns a DayOfWeek enum constant, which compares cleanly against SATURDAY and SUNDAY without any string parsing or numeric offsets to get wrong.
1// File: WeekendCheckExample.java
2import java.time.*;
3
4public class WeekendCheckExample {
5 public static void main(String[] args) {
6 LocalDate saturday = LocalDate.of(2026, 8, 29);
7 LocalDate monday = LocalDate.of(2026, 8, 31);
8
9 System.out.println("Is Aug 29 a weekend: " + isWeekend(saturday));
10 System.out.println("Is Aug 31 a weekend: " + isWeekend(monday));
11 }
12
13 static boolean isWeekend(LocalDate date) {
14 DayOfWeek day = date.getDayOfWeek();
15 return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
16 }
17}Output:
Is Aug 29 a weekend: true
Is Aug 31 a weekend: false
Creating a Date From Something Other Than Year, Month, and Day
ofEpochDay() and ofYearDay() build a LocalDate from a day count instead of individual calendar fields, which shows up whenever a date arrives as a numeric offset rather than three separate values.
1// File: AlternateCreationExample.java
2import java.time.*;
3
4public class AlternateCreationExample {
5 public static void main(String[] args) {
6 LocalDate fromEpochDay = LocalDate.ofEpochDay(0);
7 LocalDate fromYearDay = LocalDate.ofYearDay(2026, 100);
8
9 System.out.println("Epoch day 0: " + fromEpochDay);
10 System.out.println("Day 100 of 2026: " + fromYearDay);
11 }
12}Output:
Epoch day 0: 1970-01-01
Day 100 of 2026: 2026-04-10
Scheduling Reminders Relative to a Fixed Date
plusDays() chained from a single starting date is the standard way to compute a sequence of follow-up dates, such as invoice reminders.
1// File: ReminderSchedulingExample.java
2import java.time.*;
3
4public class ReminderSchedulingExample {
5 public static void main(String[] args) {
6 LocalDate invoiceDate = LocalDate.of(2026, 8, 25);
7 LocalDate firstReminder = invoiceDate.plusDays(15);
8 LocalDate finalReminder = invoiceDate.plusDays(30);
9
10 System.out.println("Invoice date: " + invoiceDate);
11 System.out.println("First reminder: " + firstReminder);
12 System.out.println("Final reminder: " + finalReminder);
13 }
14}Output:
Invoice date: 2026-08-25
First reminder: 2026-09-09
Final reminder: 2026-09-24
Handling Leap Years and Month-Length Edge Cases
plusMonths() automatically clamps to the last valid day of the target month when the original day-of-month does not exist there, correctly accounting for leap years along the way.
1// File: LeapYearEdgeCaseExample.java
2import java.time.*;
3
4public class LeapYearEdgeCaseExample {
5 public static void main(String[] args) {
6 LocalDate janEndOfLeapYear = LocalDate.of(2028, 1, 31);
7 LocalDate adjusted = janEndOfLeapYear.plusMonths(1);
8
9 System.out.println("Starting date: " + janEndOfLeapYear);
10 System.out.println("plusMonths(1): " + adjusted);
11 System.out.println("2028 is a leap year: " + Year.isLeap(2028));
12 }
13}Output:
Starting date: 2028-01-31
plusMonths(1): 2028-02-29
2028 is a leap year: true
Real-World Example
A logistics service estimating a delivery date cannot simply add a fixed number of days to an order date, since the warehouse does not process orders on Saturdays or Sundays. The estimate needs to walk forward day by day, counting only business days, and skip straight past any weekend it lands on.
1// File: DeliveryEstimator.java
2import java.time.*;
3
4public class DeliveryEstimator {
5
6 public LocalDate estimateDeliveryDate(LocalDate orderDate, int businessDays) {
7 LocalDate estimatedDate = orderDate;
8 int daysAdded = 0;
9
10 while (daysAdded < businessDays) {
11 estimatedDate = estimatedDate.plusDays(1);
12 if (!isWeekend(estimatedDate)) {
13 daysAdded++;
14 }
15 }
16
17 return estimatedDate;
18 }
19
20 private boolean isWeekend(LocalDate date) {
21 DayOfWeek day = date.getDayOfWeek();
22 return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
23 }
24}1// File: DeliveryEstimatorDemo.java
2import java.time.*;
3
4public class DeliveryEstimatorDemo {
5 public static void main(String[] args) {
6 DeliveryEstimator estimator = new DeliveryEstimator();
7
8 LocalDate orderPlacedOnFriday = LocalDate.of(2026, 8, 28);
9 LocalDate delivery = estimator.estimateDeliveryDate(orderPlacedOnFriday, 3);
10
11 System.out.println("Order placed on: " + orderPlacedOnFriday + " (" + orderPlacedOnFriday.getDayOfWeek() + ")");
12 System.out.println("Estimated delivery (3 business days): " + delivery + " (" + delivery.getDayOfWeek() + ")");
13 }
14}Output:
Order placed on: 2026-08-28 (FRIDAY)
Estimated delivery (3 business days): 2026-09-02 (WEDNESDAY)
An order placed on a Friday correctly lands three business days later on the following Wednesday, with the Saturday and Sunday in between never counted. During code reviews, seniors commonly flag a delivery estimate that just calls plusDays(businessDays) directly without checking getDayOfWeek() along the way, since that silently promises a faster delivery than the warehouse can actually fulfil across a weekend.
Combining LocalDate With Other Features
LocalDate implements Comparable<LocalDate>, so it sorts naturally inside a TreeSet or through a Stream's sorted() call with no custom Comparator needed for ordinary chronological order. LocalDate.now() depends on the system clock and default time zone, which is exactly why production code that needs to be testable typically accepts a java.time.Clock parameter instead of calling the zero-argument now() directly, letting a test substitute a fixed point in time. Paired with DateTimeFormatter, covered in the Date and Time API overview, LocalDate handles both formatting and parsing cleanly in either direction.
Best Practices
Accept an injectable Clock in production code that needs to know "today," rather than calling LocalDate.now() directly inside business logic. A test can then substitute a fixed clock and get a deterministic date every run, instead of a value that depends on when the test happens to execute.
Reach for adjuster methods like withDayOfMonth() and plusMonths() instead of manually recalculating a date field by field. LocalDate already handles month-length and leap-year edge cases correctly inside these methods, exactly as the January 31 plus one month example shows.
Use ChronoUnit.DAYS.between() for counting whole units between two dates, and Period.between() when a broken-down years, months, and days difference is what is actually needed — they answer genuinely different questions and are not interchangeable.
Validate parsed or user-supplied dates by catching DateTimeException right at the boundary where they enter the system, rather than letting an invalid date propagate deeper into business logic before it fails.
Common Mistakes
Assuming an invalid date like February 30 gets silently adjusted to the nearest valid day, the way some other date libraries behave, is a mistake that surfaces immediately as an exception instead.
1// File: InvalidDateMistake.java
2import java.time.*;
3
4public class InvalidDateMistake {
5 public static void main(String[] args) {
6 try {
7 LocalDate invalidDate = LocalDate.of(2026, 2, 30);
8 System.out.println("Never printed: " + invalidDate);
9 } catch (DateTimeException e) {
10 System.out.println("DateTimeException - February 30 does not exist, even in a leap year");
11 }
12 }
13}Output:
DateTimeException - February 30 does not exist, even in a leap year
Calling LocalDate.now() directly inside business logic makes that logic impossible to test deterministically, since the result depends entirely on when the test happens to run.
1// File: NonInjectableClockMistake.java
2import java.time.*;
3
4public class NonInjectableClockMistake {
5
6 // WRONG - hardcodes the system clock, impossible to test against a fixed date
7 static boolean isExpiredBroken(LocalDate expiryDate) {
8 return LocalDate.now().isAfter(expiryDate);
9 }
10
11 // CORRECT - accepts a Clock, letting tests substitute a fixed point in time
12 static boolean isExpired(LocalDate expiryDate, Clock clock) {
13 return LocalDate.now(clock).isAfter(expiryDate);
14 }
15
16 public static void main(String[] args) {
17 LocalDate expiryDate = LocalDate.of(2026, 1, 1);
18
19 Clock fixedClock = Clock.fixed(
20 LocalDate.of(2026, 6, 1).atStartOfDay(ZoneId.systemDefault()).toInstant(),
21 ZoneId.systemDefault()
22 );
23
24 System.out.println("Expired as of fixed test date: " + isExpired(expiryDate, fixedClock));
25 }
26}Output:
Expired as of fixed test date: true
Expecting Period.between(start, end).getDays() to return the total number of days between two dates is a very common misreading of what getDays() actually returns — it is only the leftover day count after years and months have already been subtracted out.
1// File: PeriodGetDaysMistake.java
2import java.time.*;
3import java.time.temporal.ChronoUnit;
4
5public class PeriodGetDaysMistake {
6 public static void main(String[] args) {
7 LocalDate start = LocalDate.of(2025, 1, 1);
8 LocalDate end = LocalDate.of(2026, 8, 25);
9
10 Period gap = Period.between(start, end);
11 long totalDays = ChronoUnit.DAYS.between(start, end);
12
13 // getDays() returns only the leftover day component after years and
14 // months are already accounted for - not the total number of days
15 System.out.println("Period.getDays(): " + gap.getDays());
16 System.out.println("ChronoUnit.DAYS.between(): " + totalDays);
17 }
18}Output:
Period.getDays(): 24
ChronoUnit.DAYS.between(): 601
Interview Questions
Q1. What is LocalDate, and what does it deliberately NOT represent?
LocalDate represents a calendar date — a year, a month, and a day — with no time-of-day and no time zone stored anywhere inside it. It deliberately does not represent an instant on the timeline or a specific moment in a particular location; for those concepts, LocalDateTime or ZonedDateTime are the correct types. Interviewers use this question to check whether a candidate reaches for LocalDate because it genuinely fits the data being modeled, rather than out of habit.
Q2. What happens if you try to create an invalid date like February 30 with LocalDate.of()?
It throws DateTimeException immediately, since LocalDate.of() validates the day against the actual length of the given month for the given year rather than silently rolling over into the next month. This is a deliberate design choice — an invalid date is a real error that should surface immediately, not something the API quietly papers over.
Q3. Why does LocalDate.now() make code harder to unit test, and how would you fix that?
LocalDate.now() reads the system clock, so the value it returns depends on exactly when the code runs, making any test built around it non-deterministic and impossible to pin to a specific date. The fix is to accept a java.time.Clock as a parameter and call LocalDate.now(clock) instead, which lets production code use the real system clock while tests substitute Clock.fixed() to get a predictable, repeatable date every time.
Q4. What is the difference between Period.between(date1, date2).getDays() and ChronoUnit.DAYS.between(date1, date2)?
Period.between() breaks the gap between two dates into years, months, and days, and getDays() returns only the leftover day count after the years and months portions have already been extracted — not the total span. ChronoUnit.DAYS.between() returns the actual total number of days between the two dates, with no years or months factored out separately. Confusing the two produces numbers that look plausible but are wrong by a large margin, which is exactly why product-based interviews like to ask candidates to trace through both on the same pair of dates.
Q5. How does LocalDate.plusMonths() handle a day that doesn't exist in the resulting month, like adding a month to January 31?
It clamps the result to the last valid day of the target month rather than throwing or rolling over into the following month unexpectedly. Adding one month to January 31 lands on February 28 in a common year and February 29 in a leap year, since February never has 31 days — plusMonths() already accounts for this internally, so calling code never needs to check month lengths manually.
Q6. How would you check whether a given date falls on a weekend?
Call getDayOfWeek(), which returns a DayOfWeek enum constant, and compare it against DayOfWeek.SATURDAY and DayOfWeek.SUNDAY directly. This avoids any numeric day-of-week convention entirely, sidestepping the classic off-by-one confusion between systems that start the week on Sunday versus Monday.
FAQs
Can LocalDate represent a specific time of day?
No. LocalDate has no time-of-day component at all — for a date combined with a time, LocalDateTime is the correct class, and for a date, time, and time zone together, ZonedDateTime is the correct one.
Is LocalDate thread-safe?
Yes. LocalDate is immutable, so a single instance can be shared freely across threads with no synchronization needed, since nothing about it can ever change after construction.
How do I convert a LocalDate to a java.util.Date and back?
LocalDate has no direct method for this since it carries no time-of-day or time-zone information that Date requires. The usual bridge is localDate.atStartOfDay(zoneId).toInstant() followed by Date.from(instant), and the reverse path goes through date.toInstant().atZone(zoneId).toLocalDate().
What does LocalDate.now() return if I don't pass a time zone?
It uses the JVM's default time zone to determine what "today" currently is, which can quietly differ from what a reader expects if the server's default zone does not match the zone the business logic assumes. Passing an explicit ZoneId to LocalDate.now(zoneId) removes that ambiguity entirely.
Can I use LocalDate as a key in a HashMap?
Yes, safely. LocalDate correctly overrides equals() and hashCode() based on its actual year, month, and day values, so two separately created LocalDate objects representing the same date hash and compare equal exactly as expected inside a HashMap or HashSet.
Does LocalDate account for leap years automatically?
Yes, throughout. lengthOfMonth(), plusMonths(), plusYears(), isLeapYear(), and every other date-arithmetic method on LocalDate correctly apply the Gregorian leap year rule internally, without ever needing the calling code to check for a leap year manually.
What is the difference between getMonth() and getMonthValue()?
getMonth() returns a Month enum constant, such as AUGUST, which reads clearly and avoids any numeric-indexing confusion. getMonthValue() returns the plain int from 1 to 12. Both represent the same month — the choice is about whether the calling code wants an enum for readability and type safety or a raw number for arithmetic.
Summary
LocalDate gives "just a date" its own real type instead of forcing every date-only value to carry a hidden, unused time component the way java.util.Date always did. of(), now(), and parse() create one; getYear(), getDayOfWeek(), and their relatives read it back out; and plusDays(), plusMonths(), and withDayOfMonth() return adjusted copies without ever touching the original.
The habits worth carrying forward are checking getDayOfWeek() before assuming a date arithmetic result lands on a working day, exactly the way the delivery estimator does, and keeping LocalDate.now() out of business logic that needs to be testable. LocalTime, LocalDateTime, and ZonedDateTime, covered elsewhere in this series, extend the exact same immutable, method-driven design to time and time zones.
What to Read Next
Learn how to work with a time of day, with no date or time zone.