Skip to content
elephantoo

Date & time API (java.time)

Lesson 33 of 43 15 min read

LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration, Period and formatting/parsing.


Dates and times are deceptively hard: leap years, months of different lengths, time zones, daylight saving changes, and formatting for different countries. Java's old java.util.Date and Calendar classes were mutable, confusing (January was month 0!) and not thread-safe. Since Java 8, the java.time package provides a clean, immutable and precise API. Always use it in new code.

The main classes#

ClassRepresentsExample
LocalDatea date, no time, no zone2026-10-02 (a birthday, a due date)
LocalTimea time of day, no date, no zone09:30 (opening hours)
LocalDateTimedate + time, no zone2026-10-02T09:30 (a meeting "in local time")
ZonedDateTimedate + time + time zone2026-10-02T09:30+05:30[Asia/Kolkata]
Instantan exact moment on the UTC timeline2026-10-02T04:00:00Z (log timestamps, database records)
Durationan amount of time in seconds/nanosPT1H30M (90 minutes)
Periodan amount in years/months/daysP1Y2M3D
DateTimeFormatterformatting and parsingdd/MM/yyyy

All of them are immutable and thread-safe. "Modifying" methods return new objects.

Creating dates and times#

Creating.java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;

public class Creating {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();                 // current date from the system clock
        System.out.println("Today is a valid date: " + (today.getYear() > 2000));

        LocalDate launch = LocalDate.of(2026, 10, 2);      // months are 1-12!
        LocalDate sameDay = LocalDate.of(2026, Month.OCTOBER, 2);
        LocalDate parsed = LocalDate.parse("2026-12-25");  // ISO format yyyy-MM-dd
        LocalTime standup = LocalTime.of(9, 30);
        LocalDateTime meeting = LocalDateTime.of(launch, standup);

        System.out.println(launch + " " + launch.equals(sameDay));
        System.out.println(parsed.getDayOfWeek() + " " + parsed.getDayOfYear());
        System.out.println(meeting);
        System.out.println(launch.getMonth() + " " + launch.getMonthValue() + " " + launch.getYear());
        System.out.println(launch.isLeapYear() + " " + LocalDate.of(2028, 1, 1).isLeapYear());
        System.out.println(launch.getDayOfWeek() == DayOfWeek.FRIDAY);
    }
}
Output
Today is a valid date: true
2026-10-02 true
FRIDAY 359
2026-10-02T09:30
OCTOBER 10 2026
false true
true

Invalid values fail fast: LocalDate.of(2026, 2, 30) throws DateTimeException: Invalid date 'FEBRUARY 30'.

Date arithmetic#

Arithmetic.java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;

public class Arithmetic {
    public static void main(String[] args) {
        LocalDate d = LocalDate.of(2026, 1, 31);

        System.out.println(d.plusDays(10));
        System.out.println(d.plusMonths(1));              // clamps to the end of February
        System.out.println(d.minusYears(2).plusWeeks(1));
        System.out.println(d.withDayOfMonth(1));          // first of the month
        System.out.println(d.with(TemporalAdjusters.lastDayOfYear()));
        System.out.println(d.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));

        LocalDate start = LocalDate.of(2026, 3, 15);
        LocalDate end = LocalDate.of(2026, 10, 2);
        System.out.println(start.isBefore(end) + " " + start.isAfter(end));
        System.out.println(ChronoUnit.DAYS.between(start, end) + " days");
        System.out.println(ChronoUnit.WEEKS.between(start, end) + " weeks");
    }
}
Output
2026-02-10
2026-02-28
2024-02-07
2026-01-01
2026-12-31
2026-02-02
true false
201 days
28 weeks

Because dates are immutable, d.plusDays(10) on its own line does nothing useful. Assign the result: d = d.plusDays(10);.

Period and Duration#

Amounts.java
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;

public class Amounts {
    public static void main(String[] args) {
        LocalDate birthday = LocalDate.of(1998, 7, 14);
        LocalDate onDate = LocalDate.of(2026, 10, 2);
        Period age = Period.between(birthday, onDate);
        System.out.println(age.getYears() + " years, " + age.getMonths() + " months, " + age.getDays() + " days");
        System.out.println(age);

        Period trial = Period.ofDays(14);
        System.out.println("Trial ends " + onDate.plus(trial));

        Duration film = Duration.ofMinutes(165);
        System.out.println(film + " = " + film.toHours() + "h " + film.toMinutesPart() + "m");

        LocalDateTime checkIn = LocalDateTime.of(2026, 10, 2, 14, 0);
        LocalDateTime checkOut = LocalDateTime.of(2026, 10, 4, 11, 30);
        Duration stay = Duration.between(checkIn, checkOut);
        System.out.println("Stay: " + stay.toHours() + " hours");

        System.out.println(LocalTime.of(23, 45).plus(Duration.ofMinutes(30)));   // wraps past midnight
    }
}
Output
28 years, 2 months, 18 days
P28Y2M18D
Trial ends 2026-10-16
PT2H45M = 2h 45m
Stay: 45 hours
00:15

Formatting and parsing#

DateTimeFormatter converts between dates and text. Patterns use letters like dd (day), MM (month number), MMM (short month name), yyyy (year), HH (24-hour), hh (12-hour), mm (minutes), a (AM/PM):

Formatting.java
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;

public class Formatting {
    public static void main(String[] args) {
        LocalDateTime t = LocalDateTime.of(2026, 10, 2, 18, 5);

        DateTimeFormatter indian = DateTimeFormatter.ofPattern("dd/MM/yyyy");
        DateTimeFormatter friendly = DateTimeFormatter.ofPattern("EEEE, d MMMM yyyy 'at' h:mm a", Locale.ENGLISH);
        DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

        System.out.println(t.format(indian));
        System.out.println(t.format(friendly));
        System.out.println(t.format(iso));

        LocalDate parsed = LocalDate.parse("25/12/2026", indian);
        System.out.println(parsed);

        try {
            LocalDate.parse("2026/12/25", indian);
        } catch (DateTimeParseException e) {
            System.out.println("Could not parse: " + e.getParsedString());
        }
    }
}
Output
02/10/2026
Friday, 2 October 2026 at 6:05 PM
2026-10-02T18:05:00
2026-12-25
Could not parse: 2026/12/25

Beware MM (month) vs mm (minutes), and yyyy (year) vs YYYY (week-based year, which gives wrong results around New Year). Formatters are immutable and thread-safe, so store them in static final constants.

Time zones: ZonedDateTime and Instant#

LocalDateTime has no zone, so "2026-10-02 09:00" means different moments in Mumbai and London. When the actual moment matters (meetings across countries, flights, logs), use zones:

Zones.java
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Duration;

public class Zones {
    public static void main(String[] args) {
        ZoneId india = ZoneId.of("Asia/Kolkata");
        ZoneId newYork = ZoneId.of("America/New_York");
        ZoneId london = ZoneId.of("Europe/London");

        ZonedDateTime call = ZonedDateTime.of(LocalDateTime.of(2026, 10, 2, 19, 30), india);
        System.out.println("India:    " + call);
        System.out.println("New York: " + call.withZoneSameInstant(newYork));
        System.out.println("London:   " + call.withZoneSameInstant(london));

        Instant moment = call.toInstant();               // the same moment in UTC
        System.out.println("UTC:      " + moment);
        System.out.println(moment.toEpochMilli());

        Instant fromDb = Instant.parse("2026-10-02T04:00:00Z");
        System.out.println(fromDb.atZone(india).toLocalDateTime());

        // Daylight saving: clocks in New York go back on 1 November 2026
        ZonedDateTime before = ZonedDateTime.of(LocalDateTime.of(2026, 10, 31, 12, 0), newYork);
        ZonedDateTime after = before.plusDays(1);
        System.out.println(Duration.between(before, after).toHours() + " hours in that 'day'");
    }
}
Output
India:    2026-10-02T19:30+05:30[Asia/Kolkata]
New York: 2026-10-02T10:00-04:00[America/New_York]
London:   2026-10-02T15:00+01:00[Europe/London]
UTC:      2026-10-02T14:00:00Z
1790949600000
2026-10-02T09:30
25 hours in that 'day'

Best practice for applications:

  • Store and transmit Instants (UTC): in databases, APIs and logs.
  • Convert to ZonedDateTime at the edges, for display in the user's zone.
  • Use region IDs like Asia/Kolkata, not fixed offsets like +05:30, so daylight-saving rules are handled.
  • Use LocalDate for things that are genuinely zone-independent, such as birthdays and holidays.

Measuring elapsed time#

For timing code, don't use wall-clock dates; use System.nanoTime() or Instant with Duration:

Java
long start = System.nanoTime();
doWork();
long elapsedMs = (System.nanoTime() - start) / 1_000_000;

Instant t0 = Instant.now();
doWork();
System.out.println(Duration.between(t0, Instant.now()).toMillis() + " ms");

Testable code with Clock#

LocalDate.now() makes code hard to test, because the answer changes every day. Accept a Clock instead:

Subscription.java
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;

public class Subscription {
    private final LocalDate expiry;
    private final Clock clock;

    Subscription(LocalDate expiry, Clock clock) {
        this.expiry = expiry;
        this.clock = clock;
    }

    boolean isActive() {
        return !LocalDate.now(clock).isAfter(expiry);
    }

    public static void main(String[] args) {
        Clock fixed = Clock.fixed(Instant.parse("2026-10-02T06:00:00Z"), ZoneId.of("Asia/Kolkata"));
        System.out.println(new Subscription(LocalDate.of(2026, 10, 2), fixed).isActive());
        System.out.println(new Subscription(LocalDate.of(2026, 10, 1), fixed).isActive());
        // In production: new Subscription(expiry, Clock.systemDefaultZone())
    }
}
Output
true
false

Converting legacy types#

You'll still meet java.util.Date in older libraries and java.sql.Timestamp in old JDBC code:

Java
Date legacy = new Date();
Instant instant = legacy.toInstant();
Date back = Date.from(instant);

LocalDate d = java.sql.Date.valueOf("2026-10-02").toLocalDate();

Modern JDBC drivers (and JPA) map LocalDate, LocalDateTime and Instant directly, so you rarely need these conversions.

Common mistakes#

  • Using Date/Calendar/SimpleDateFormat in new code (SimpleDateFormat is not thread-safe).
  • Ignoring the return value of plusDays and friends.
  • Using LocalDateTime for moments that span time zones; use Instant or ZonedDateTime.
  • Mixing up mm/MM and yyyy/YYYY in patterns.
  • Calling now() all over business logic instead of injecting a Clock.

What's next#

Next we put data on disk and read it back: file I/O with java.nio.

Check your understanding

Quick quiz

0/3 answered
  1. 1.Which class represents a date without a time or time zone, such as a birthday?

  2. 2.What does LocalDate.of(2026, 1, 31).plusMonths(1) return?

  3. 3.What is the difference between Period and Duration?

Finished reading?

Mark this lesson complete to track your progress.