LocalDate and LocalTime

Model calendar dates and wall-clock times with LocalDate and LocalTime, and know when a value with no time zone is the right choice.

SyntaxLocalDate d = LocalDate.of(2026, 10, 13); // or LocalDate.parse("2026-10-13") LocalTime t = LocalTime.of(9, 30); LocalDate later = d.plusDays(30); // returns a new value boolean early = t.isBefore(LocalTime.NOON);

java.time splits what older code crammed into java.util.Date into separate types, each holding exactly one idea. LocalDate is a date on a calendar with no time and no zone: 2026-10-13. LocalTime is a time on a wall clock with no date and no zone: 09:30. Both are immutable and thread-safe, so you can keep them in static fields, share them between requests and use them as map keys.

When "local" is the right answer

Local means "not tied to any place". That fits values that people agree on as text, wherever they are: a date of birth, an invoice date, a public holiday, a shop's opening hour, the day a subscription renews. A birthday is 12 March for the person whatever server reads it; converting it to UTC would only add a chance of shifting it by a day.

It does not fit moments in time. "The order was placed at 09:30" means nothing until you know where. For those, use Instant or ZonedDateTime, covered in the next lessons.

Creating and changing values

LocalDate due = LocalDate.of(2026, 1, 31);   // months are 1-12
LocalDate next = due.plusMonths(1);          // 2026-02-28, a new object
LocalTime open = LocalTime.parse("08:30");

Every "change" returns a new value. Calling due.plusMonths(1); on its own line does nothing useful; you must keep the result. Month arithmetic clamps to the end of the month, so 31 January plus one month is 28 February, and adding another month gives 28 March, not 31 March.

Invalid values fail fast: LocalDate.of(2026, 2, 30) throws DateTimeException with the message Invalid date 'FEBRUARY 30'. That makes these types a good first line of validation for request input.

Mistakes people make

  • Calling LocalDate.now() with no argument. It uses the JVM's default zone, which is usually UTC in a container and something else on a laptop. A server in UTC thinks it is still yesterday for a user in Sydney. Pass a zone or a Clock.
  • Comparing with equals when you mean order. Use isBefore, isAfter and isEqual, or compareTo for sorting.
  • Forgetting that LocalTime wraps. LocalTime.of(23, 30).plusHours(1) is 00:30, with no hint that the day changed. Opening hours that cross midnight need explicit handling.

Databases

Through JDBC 4.2, a SQL DATE column maps to LocalDate and TIME maps to LocalTime: rs.getObject("due_on", LocalDate.class). There is no need for java.sql.Date in new code.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Store an invoice date or a user's date of birth as a SQL DATE read with getObject(column, LocalDate.class).
  • Compute subscription renewal dates from the original start date so month-end customers are not drifted to the 28th.
  • Check whether a store's pickup slot falls inside its opening hours before accepting a click-and-collect order.

More examples

Validating a date from a request

LocalDate.of rejects impossible dates itself, so the handler only adds the business rule. Passing today in, rather than calling LocalDate.now() inside, keeps the method deterministic and testable.

Example · java
String validateDob(int year, int month, int day, LocalDate today) {
    try {
        LocalDate dob = LocalDate.of(year, month, day);
        if (dob.isAfter(today)) {
            return "400 date of birth is in the future";
        }
        return "200 " + dob;
    } catch (DateTimeException e) {
        return "400 " + e.getMessage();
    }
}

void main() {
    LocalDate today = LocalDate.of(2026, 10, 13);
    IO.println(validateDob(1990, 3, 12, today));
    IO.println(validateDob(2026, 2, 30, today));
    IO.println(validateDob(2027, 1, 1, today));
    IO.println(validateDob(2026, 13, 1, today));
}
// Output:
// 200 1990-03-12
// 400 Invalid date 'FEBRUARY 30'
// 400 date of birth is in the future
// 400 Invalid value for MonthOfYear (valid values 1 - 12): 13

Monthly renewals without drift

Once February clamps the day to 28, chaining keeps it there for every later month. Computing start.plusMonths(n) each time renews on the last day of the month as the customer expects.

Example · java
void main() {
    LocalDate start = LocalDate.of(2026, 1, 31);

    // Wrong: each step clamps, and the clamped day sticks.
    LocalDate chained = start;
    for (int i = 1; i <= 3; i++) {
        chained = chained.plusMonths(1);
        IO.println("chained  " + i + ": " + chained);
    }

    // Right: always count from the original anchor date.
    for (int i = 1; i <= 3; i++) {
        IO.println("anchored " + i + ": " + start.plusMonths(i));
    }
}
// Output:
// chained  1: 2026-02-28
// chained  2: 2026-03-28
// chained  3: 2026-04-28
// anchored 1: 2026-02-28
// anchored 2: 2026-03-31
// anchored 3: 2026-04-30

Opening hours that cross midnight

A naive 'after opens and before closes' check is never true when closing time is earlier than opening time. Treating the overnight case separately keeps the rule correct and the end time exclusive.

Example · java
record Hours(LocalTime opens, LocalTime closes) {
    boolean isOpen(LocalTime t) {
        if (opens.isBefore(closes)) {
            return !t.isBefore(opens) && t.isBefore(closes);
        }
        // Overnight: open from opens to midnight, and from midnight to closes.
        return !t.isBefore(opens) || t.isBefore(closes);
    }
}

void main() {
    Hours cafe = new Hours(LocalTime.of(8, 0), LocalTime.of(18, 0));
    Hours bar = new Hours(LocalTime.of(18, 0), LocalTime.of(2, 0));
    for (String s : List.of("07:59", "12:00", "23:15", "01:30", "02:00")) {
        LocalTime t = LocalTime.parse(s);
        IO.println(t + " cafe=" + cafe.isOpen(t) + " bar=" + bar.isOpen(t));
    }
}
// Output:
// 07:59 cafe=false bar=false
// 12:00 cafe=true bar=false
// 23:15 cafe=false bar=true
// 01:30 cafe=false bar=true
// 02:00 cafe=false bar=false

Discussion

  • Be the first to comment on this lesson.