The java.time API: LocalDate, LocalTime and LocalDateTime

The modern date and time API is immutable, thread safe and clearly named, and it replaced the older Date and Calendar classes entirely.

Why java.time exists

The original java.util.Date and Calendar were mutable, not thread safe, and confusingly designed: months counted from zero, years counted from 1900, and a Date actually represented an instant rather than a date. Java 8 introduced java.time, which fixed all of it.

OldModern replacement
java.util.DateInstant or LocalDateTime
CalendarLocalDate, ZonedDateTime
SimpleDateFormatDateTimeFormatter
java.sql.DateLocalDate
Every type in java.time is immutable. Every method that appears to modify a value in fact returns a new one, which makes these types safe to share between threads and safe as map keys.

Choosing the right type

TypeHoldsUse for
LocalDateA date, no time, no zoneA birthday, an invoice date
LocalTimeA time, no date, no zoneAn opening time
LocalDateTimeDate and time, no zoneA local appointment
ZonedDateTimeDate, time and a zoneA meeting across regions
OffsetDateTimeDate, time and a fixed offsetTimestamps in APIs and databases
InstantA point on the timeline, in UTCLogging, measuring, storage
DurationA time based amountSeconds, minutes, hours
PeriodA date based amountYears, months, days
Year, YearMonth, MonthDayPartial datesCard expiry, anniversaries

Creating values

LocalDate today = LocalDate.now();
LocalDate independence = LocalDate.of(1947, 8, 15);
LocalDate typed = LocalDate.of(2026, Month.AUGUST, 21);
LocalDate parsed = LocalDate.parse("2026-08-21");        // ISO format by default

LocalTime opening = LocalTime.of(9, 30);
LocalTime precise = LocalTime.of(9, 30, 15, 500_000_000);

LocalDateTime meeting = LocalDateTime.of(2026, 8, 21, 14, 0);
LocalDateTime combined = today.atTime(opening);
LocalDate justTheDate = meeting.toLocalDate();

Months are numbered from 1, as everyone always expected. The Month enum removes the ambiguity entirely.

Reading parts

LocalDate date = LocalDate.of(2026, 8, 21);

System.out.println(date.getYear());            // 2026
System.out.println(date.getMonth());           // AUGUST
System.out.println(date.getMonthValue());      // 8
System.out.println(date.getDayOfMonth());      // 21
System.out.println(date.getDayOfWeek());       // FRIDAY
System.out.println(date.getDayOfYear());       // 233
System.out.println(date.lengthOfMonth());      // 31
System.out.println(date.isLeapYear());         // false

Arithmetic returns new values

LocalDate date = LocalDate.of(2026, 8, 21);

LocalDate nextWeek = date.plusWeeks(1);
LocalDate lastMonth = date.minusMonths(1);
LocalDate nextYear = date.plusYears(1);

date.plusDays(10);                  // result discarded, date is unchanged
LocalDate later = date.plusDays(10); // correct
LocalDate endOfMonth = LocalDate.of(2026, 1, 31);
System.out.println(endOfMonth.plusMonths(1));   // 2026-02-28, clamped safely

Adding a month to 31 January gives the last valid day of February rather than an error or an overflow into March. The API resolves these cases predictably.

The with methods

LocalDate date = LocalDate.of(2026, 8, 21);

System.out.println(date.withDayOfMonth(1));            // 2026-08-01
System.out.println(date.withMonth(12));                // 2026-12-21

System.out.println(date.with(TemporalAdjusters.firstDayOfMonth()));
System.out.println(date.with(TemporalAdjusters.lastDayOfMonth()));
System.out.println(date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));
System.out.println(date.with(TemporalAdjusters.firstDayOfNextMonth()));

Comparing

LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 12, 31);
LocalDate today = LocalDate.now();

System.out.println(today.isAfter(start));
System.out.println(today.isBefore(end));
System.out.println(today.isEqual(start));
System.out.println(start.compareTo(end));      // negative

boolean inRange = !today.isBefore(start) && !today.isAfter(end);

Note that isBefore and isAfter are strict. For an inclusive range, negate the opposite test as above.

A worked example

public record Subscription(LocalDate startedOn, int months) {

    public LocalDate expiresOn() {
        return startedOn.plusMonths(months);
    }

    public boolean isActiveOn(LocalDate date) {
        return !date.isBefore(startedOn) && date.isBefore(expiresOn());
    }

    public long daysRemaining(LocalDate from) {
        return Math.max(0, ChronoUnit.DAYS.between(from, expiresOn()));
    }
}
Subscription plan = new Subscription(LocalDate.of(2026, 1, 15), 12);
System.out.println(plan.expiresOn());                      // 2027-01-15
System.out.println(plan.isActiveOn(LocalDate.now()));
System.out.println(plan.daysRemaining(LocalDate.now()));

Measuring the gap

LocalDate birth = LocalDate.of(1998, 3, 12);
LocalDate today = LocalDate.now();

long days = ChronoUnit.DAYS.between(birth, today);
long months = ChronoUnit.MONTHS.between(birth, today);
int age = Period.between(birth, today).getYears();

Common mistakes

  • Discarding the result of plusDays and expecting the original to change.
  • Using LocalDateTime for a timestamp that must be comparable across zones. Use Instant or OffsetDateTime.
  • Reaching for Date, Calendar or SimpleDateFormat in new code.
  • Assuming a day is always 24 hours, which fails across a daylight saving change.
  • Comparing dates with == rather than equals or isEqual.

Best practices

  • Use LocalDate when there is no time, and do not invent a midnight time.
  • Store timestamps as Instant or in UTC, and convert to a zone only for display.
  • Use Month and DayOfWeek rather than raw integers.
  • Pass a Clock into code that needs the current time, so it can be tested.
  • Keep java.time types in your domain model instead of converting to strings early.
public class InvoiceService {

    private final Clock clock;

    public InvoiceService(Clock clock) {
        this.clock = clock;
    }

    public LocalDate dueDate() {
        return LocalDate.now(clock).plusDays(30);
    }
}

// In a test
InvoiceService service = new InvoiceService(
        Clock.fixed(Instant.parse("2026-08-21T00:00:00Z"), ZoneOffset.UTC));

Practice

  1. Calculate the number of days until the next new year from today.
  2. Why does LocalDate.of(2026, 1, 31).plusMonths(1) return 28 February?
  3. Write a method returning the first Monday of a given month.
  4. Explain when LocalDateTime is the wrong choice, with an example.
  5. Make a method that uses the current date testable by injecting a Clock.

Conclusion

Pick the type that matches exactly what you know: a date, a time, both, or a point on the timeline. Everything is immutable, so keep the returned value, and inject a Clock whenever the current time is involved.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.