11.1 Date and Time Core Types: `LocalDate`, `LocalTime`, `LocalDateTime`

Key Takeaways

  • The java.time framework (JSR 310) replaces legacy mutable types with immutable, thread-safe, value-based classes instantiated exclusively through private constructors and static factory methods.

  • LocalDate models year, month, and day; LocalTime models hour, minute, second, and nanosecond; LocalDateTime unites both date and time into a single zone-agnostic composite representation.

  • Month values in java.time are strictly 1-based (1 is January, 12 is December) and support both integer primitives and the Month enum; invalid temporal parameters throw an unchecked DateTimeException at runtime.

  • All temporal modification methods (plusDays, minusMonths, withYear) return newly instantiated objects; ignoring the return value leaves the original temporal instance unmodified.

  • Temporal field queries via ChronoField compile on any TemporalAccessor, but querying unsupported fields (such as time fields on a LocalDate) triggers an UnsupportedTemporalTypeException at runtime.

Last updated: October 2026

11.1 Date and Time Core Types: LocalDate, LocalTime, LocalDateTime

Prior to Java SE 8, date and time manipulation in Java was plagued by design flaws in java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat. These legacy classes were mutable, non-thread-safe, zero-indexed for months, and inconsistent in naming conventions (such as java.util.Date actually representing a combined date and millisecond timestamp).

Java SE 8 introduced the modern Date-Time API under the java.time package (formalized via JSR 310 and maintained in Java SE 17). This modern framework adheres to strict software engineering standards:

  • Immutability and Thread Safety: Every core temporal class is immutable and value-based. Once instantiated, an object's internal state cannot be modified, making instances inherently thread-safe without synchronization.
  • Private Constructors and Static Factories: None of the core temporal classes provide public constructors. Instantiation is performed exclusively through standardized static factory methods (such as of(), now(), from(), and parse()).
  • Clear Domain Separation: Human calendar concepts (dates, times, time zones) are strictly separated from machine timeline timestamps (Instant).

Core Local Temporal Representations

The java.time package provides three core types for managing date and time information without timezone offsets:

ClassFields ContainedISO-8601 Default FormatSample Value
LocalDateYear, Month, Dayuuuu-MM-dd2026-04-15
LocalTimeHour, Minute, Second, NanosecondHH:mm:ss.nnnnnnnnn14:30:45.125
LocalDateTimeYear, Month, Day, Hour, Minute, Second, Nanoseconduuuu-MM-ddTHH:mm:ss.nnnnnnnnn2026-04-15T14:30:45.125

These classes are zone-agnostic (also termed context-free). A LocalDate representing 2026-04-15 denotes April 15, 2026 regardless of whether it is viewed in Tokyo, London, or New York. It cannot represent an exact physical instant on the universal timeline because it does not carry a timezone offset.


Instantiation via Static Factory Methods

Attempting to invoke new LocalDate() or new LocalTime() results in a compilation error because all constructors in java.time are declared private.

Current System Time: now()

The now() factory method queries the system clock in the default timezone:

LocalDate today = LocalDate.now();
LocalTime thisMoment = LocalTime.now();
LocalDateTime currentDateTime = LocalDateTime.now();

Explicit Instantiation: of(...)

The of(...) factory method creates instances from explicit numeric or enumerated components:

// Using 1-based integer month (1 = January, 12 = December)
LocalDate taxDay1 = LocalDate.of(2026, 4, 15);

// Using java.time.Month enumeration
LocalDate taxDay2 = LocalDate.of(2026, Month.APRIL, 15);

// LocalTime overloads:
LocalTime meetingTime = LocalTime.of(14, 30);                // 14:30:00
LocalTime precisionTime = LocalTime.of(14, 30, 45);          // 14:30:45
LocalTime nanoTime = LocalTime.of(14, 30, 45, 125_000_000);  // 14:30:45.125

// LocalDateTime overloads:
LocalDateTime flightDeparture = LocalDateTime.of(2026, Month.APRIL, 15, 14, 30);
LocalDateTime event = LocalDateTime.of(taxDay1, meetingTime);

Note

Unlike java.util.Calendar where January was represented by 0, the java.time API uses standard 1-based indexing for months. Passing 0 or 13 to LocalDate.of(2026, month, 15) triggers a runtime exception.

Combining and Extracting Components

You can compose and decompose temporal instances fluently:

LocalDate date = LocalDate.of(2026, Month.MAY, 1);
LocalTime time = LocalTime.of(9, 15);

// Composing into LocalDateTime
LocalDateTime ldt1 = date.atTime(time);
LocalDateTime ldt2 = date.atTime(9, 15, 30);
LocalDateTime ldt3 = time.atDate(date);

// Decomposing from LocalDateTime
LocalDate extractedDate = ldt1.toLocalDate();
LocalTime extractedTime = ldt1.toLocalTime();

Invalid Values and DateTimeException

In legacy java.util.Calendar, passing invalid date values (such as February 30) caused the calendar to silently roll over into March under lenient mode. In java.time, all parameters are strictly validated at runtime. If any component falls outside its legal astronomical range, Java throws an unchecked java.time.DateTimeException (a subclass of RuntimeException).

// Leap year validation: 2026 is NOT a leap year (2026 is not divisible by 4)
// Throws DateTimeException: Invalid date 'February 29' as '2026' is not a leap year
LocalDate leapCheck = LocalDate.of(2026, Month.FEBRUARY, 29); 

// Day of month boundary validation: April has 30 days
// Throws DateTimeException: Invalid date 'APRIL 31'
LocalDate aprilCheck = LocalDate.of(2026, Month.APRIL, 31); 

// 24-hour clock boundary: Hours range from 0 to 23
// Throws DateTimeException: Invalid value for HourOfDay (valid values 0 - 23): 24
LocalTime hourCheck = LocalTime.of(24, 0); 

Warning

Always verify whether a year in an exam question is a leap year before accepting February 29! A year is a leap year if it is divisible by 4, except end-of-century years which must be divisible by 400 (e.g., 2000 and 2024 are leap years; 1900, 2025, and 2026 are not).


Immutability Mechanics and Modification Methods

All instances of LocalDate, LocalTime, and LocalDateTime are completely immutable. Any method that appears to modify an instance returns a brand new object with the updated values. If the returned reference is not assigned, the original object remains unchanged.

Common Manipulation Methods

Each class provides fluent arithmetic and replacement methods:

  • Addition: plusDays(long), plusWeeks(long), plusMonths(long), plusYears(long), plusHours(long), plusMinutes(long), plusSeconds(long), plusNanos(long)
  • Subtraction: minusDays(long), minusMonths(long), minusYears(long), minusHours(long), minusMinutes(long), etc.
  • Direct Replacement (with...): withYear(int), withMonth(int), withDayOfMonth(int), withHour(int), withMinute(int), etc.
LocalDate original = LocalDate.of(2026, Month.JANUARY, 10);
LocalDate nextMonth = original.plusMonths(1);       // 2026-02-10
LocalDate yearLater = original.plusYears(1);        // 2027-01-10
LocalDate alteredDay = original.withDayOfMonth(25);  // 2026-01-25

The Ignored Return Value Trap

A classic trap is the unassigned return value:

LocalDate holiday = LocalDate.of(2026, Month.JULY, 4);
holiday.plusDays(10); // Return value discarded!
holiday.plusMonths(1); // Return value discarded!
System.out.println(holiday); // PRINTS: 2026-07-04 (Completely unmodified!)

Because LocalDate is immutable, holiday.plusDays(10) creates a new LocalDate containing July 14, 2026, which is immediately eligible for garbage collection because no variable references it. The variable holiday still points to the initial instance.


Field Extraction and ChronoField

Java provides dedicated getter methods alongside a generalized temporal query mechanism.

Dedicated Getters

  • getYear(): returns primitive int
  • getMonth(): returns Month enum constant (Month.JANUARY through Month.DECEMBER)
  • getMonthValue(): returns primitive int from 1 to 12
  • getDayOfMonth(): returns primitive int from 1 to 31
  • getDayOfWeek(): returns DayOfWeek enum constant (DayOfWeek.MONDAY through DayOfWeek.SUNDAY)
  • getDayOfYear(): returns primitive int from 1 to 365 (or 366 in leap years)
  • getHour(), getMinute(), getSecond(), getNano(): return corresponding primitive int values
LocalDate sample = LocalDate.of(2026, Month.APRIL, 15);
System.out.println(sample.getMonth());      // APRIL (enum)
System.out.println(sample.getMonthValue()); // 4 (int)
System.out.println(sample.getDayOfWeek());  // WEDNESDAY (enum)

The TemporalAccessor Interface and ChronoField

LocalDate, LocalTime, and LocalDateTime all implement java.time.temporal.TemporalAccessor, exposing get(TemporalField) and getLong(TemporalField). The standard implementation of TemporalField is the java.time.temporal.ChronoField enum:

LocalDate date = LocalDate.of(2026, Month.APRIL, 15);
int year = date.get(ChronoField.YEAR);               // 2026
int day = date.get(ChronoField.DAY_OF_MONTH);        // 15
long epochDay = date.getLong(ChronoField.EPOCH_DAY); // Days since 1970-01-01

The UnsupportedTemporalTypeException Trap

Because the get(TemporalField) signature accepts any TemporalField, you can pass a time field to a LocalDate or a date field to a LocalTime. This code compiles cleanly because both types implement TemporalAccessor. However, at runtime, the method throws java.time.temporal.UnsupportedTemporalTypeException:

LocalTime time = LocalTime.of(12, 30);
// Compiles! But throws UnsupportedTemporalTypeException at runtime:
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: DayOfMonth
int day = time.get(ChronoField.DAY_OF_MONTH); 

LocalDate date = LocalDate.of(2026, 4, 15);
// Compiles! But throws UnsupportedTemporalTypeException at runtime:
// java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: HourOfDay
int hour = date.get(ChronoField.HOUR_OF_DAY); 

To safely inspect whether a temporal object supports a given field, invoke isSupported(TemporalField) beforehand:

if (date.isSupported(ChronoField.HOUR_OF_DAY)) {
    int h = date.get(ChronoField.HOUR_OF_DAY);
} else {
    // Safely branch when the field is unsupported
}
Test Your Knowledge

Consider the following code snippet:

LocalDate date = LocalDate.of(2026, Month.APRIL, 30);
date.plusDays(2);
date.plusMonths(1);
System.out.println(date.getMonth() + " " + date.getDayOfMonth());

What is printed when this program executes?

A

Throws a DateTimeException at runtime

B

APRIL 30

C

MAY 2

D

JUNE 2

Test Your Knowledge

What is the result of attempting to compile and run the following code?

LocalDate d1 = LocalDate.of(2024, Month.FEBRUARY, 29);
LocalTime t1 = LocalTime.of(23, 59, 59);
LocalDate d2 = LocalDate.of(2025, Month.FEBRUARY, 29);
System.out.println(d1 + " " + t1);
A

Throws a DateTimeException at runtime when evaluating d2

B

Throws an UnsupportedTemporalTypeException at runtime when initializing d1

C

Prints 2024-02-29 23:59:59 and silently rolls d2 over to 2025-03-01

D

Compilation fails on LocalTime.of(23, 59, 59) because nanoseconds must be specified

Test Your Knowledge

Consider the following code snippet:

LocalTime time = LocalTime.of(15, 30);
LocalDate date = LocalDate.of(2026, 6, 1);
System.out.print(date.get(ChronoField.DAY_OF_MONTH) + " ");
System.out.print(time.get(ChronoField.HOUR_OF_DAY) + " ");
System.out.print(time.get(ChronoField.DAY_OF_YEAR));

What is the outcome of compiling and running this code?

A

Prints 1 15 152

B

Prints 1 15 0

C

Compilation fails at time.get(ChronoField.DAY_OF_YEAR) because ChronoField cannot be used with LocalTime

D

Prints 1 15 and then throws an UnsupportedTemporalTypeException at runtime

Sections you finish are checked off in the contents.