Date & Time #

Before Java 8, working with dates and times was a nightmare. java.util.Date could represent both a date and a time but not each separately. Calendar was verbose and bug-prone. Both were mutable — meaning a date object could be changed from anywhere, causing bugs that were hard to trace. Java 8 introduced java.time — a new API redesigned from scratch, immutable, thread-safe, and far more intuitive. This article covers all the main classes in java.time: how to create, manipulate, compare, format, and convert dates and times — including time zone handling and duration measurement.

java.time Overview #

The java.time package has one main principle: each class represents exactly one concept. There’s no “jack-of-all-trades” class like the old ambiguous Date.

ClassRepresentsExample
LocalDateDate only, without time2025-08-17
LocalTimeTime only, without date14:30:00
LocalDateTimeDate + time, without zone2025-08-17T14:30:00
ZonedDateTimeDate + time + time zone2025-08-17T14:30:00+07:00[Asia/Jakarta]
InstantA point in time in UTC (epoch)2025-08-17T07:30:00Z
DurationTime difference in hours/minutes/secondsPT4H30M (4 hours 30 minutes)
PeriodDate difference in days/months/yearsP1Y2M3D (1 year 2 months 3 days)

All classes in java.time are immutable — every operation like plusDays() or minusHours() doesn’t change the original object, but returns a new one. This makes them safe to use in multithreading environments without extra synchronization.

flowchart LR
    A["LocalDate\n(date only)"] --> D["LocalDateTime\n(date + time)"]
    B["LocalTime\n(time only)"] --> D
    D --> E["ZonedDateTime\n(+ time zone)"]
    E --> F["Instant\n(UTC epoch)"]
Avoid java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat in new code. All three are mutable, not thread-safe, and full of traps. java.time is their replacement since Java 8.

LocalDate #

LocalDate represents a date — year, month, day — without any time component. Use it for birth dates, due dates, event dates, or any data that doesn’t need hour information.

Creating a LocalDate #

import java.time.LocalDate;
import java.time.Month;

// Today's date from the system clock
LocalDate today = LocalDate.now();
System.out.println(today); // 2025-05-07

// Specific date — month uses a number (1-12)
LocalDate independence = LocalDate.of(1945, 8, 17);
LocalDate holiday     = LocalDate.of(2025, Month.MARCH, 30); // can use the Month enum

// Parsing from an ISO-8601 string
LocalDate fromString = LocalDate.parse("2025-12-31");

// From the N-th day of the year (day-of-year)
LocalDate day100 = LocalDate.ofYearDay(2025, 100); // April 10, 2025

Reading Components #

LocalDate date = LocalDate.of(2025, 8, 17);

int year  = date.getYear();        // 2025
int month  = date.getMonthValue();  // 8
int day   = date.getDayOfMonth();  // 17

Month monthName  = date.getMonth();       // AUGUST
DayOfWeek dayOfWeek = date.getDayOfWeek(); // SUNDAY

int dayOfYear = date.getDayOfYear(); // 229
int daysInMonth = date.lengthOfMonth(); // 31 (August has 31 days)
boolean isLeapYear = date.isLeapYear(); // false

Manipulating Dates #

Because LocalDate is immutable, all manipulation methods return a new object. The original object doesn’t change.

LocalDate start = LocalDate.of(2025, 1, 15);

// Add and subtract
LocalDate weekLater   = start.plusWeeks(1);    // 2025-01-22
LocalDate monthAgo    = start.minusMonths(1);  // 2024-12-15
LocalDate yearLater = start.plusYears(1);   // 2026-01-15

// Set specific components (withXxx replaces a specific component)
LocalDate changeMonth = start.withMonth(6);        // 2025-06-15
LocalDate changeDay  = start.withDayOfMonth(1);   // 2025-01-01

// First and last day of the month
LocalDate first = start.withDayOfMonth(1);
LocalDate last = start.withDayOfMonth(start.lengthOfMonth());

Comparing Dates #

LocalDate a = LocalDate.of(2025, 1, 1);
LocalDate b = LocalDate.of(2025, 6, 15);

boolean aBefore  = a.isBefore(b);  // true
boolean aAfter   = a.isAfter(b);   // false
boolean aEqual   = a.isEqual(b);   // false
int     diff  = a.compareTo(b); // negative if a is earlier

// Check whether a date is within a range
LocalDate now = LocalDate.now();
boolean inRange = !now.isBefore(a) && !now.isAfter(b);

LocalTime #

LocalTime represents a time of day — hours, minutes, seconds, nanoseconds — without date or time zone information. Good for store opening hours, meeting schedules, or alarm times.

Creating a LocalTime #

import java.time.LocalTime;

// Current time
LocalTime now = LocalTime.now();

// Specific time — parameters: hour, minute, second (optional), nanosecond (optional)
LocalTime officeHours  = LocalTime.of(9, 0);          // 09:00
LocalTime lunch      = LocalTime.of(12, 30, 0);     // 12:30:00
LocalTime precise    = LocalTime.of(14, 0, 0, 500_000_000); // 14:00:00.5

// Boundary values
LocalTime midnight = LocalTime.MIDNIGHT; // 00:00
LocalTime noon  = LocalTime.NOON;     // 12:00

// Parsing from a string
LocalTime fromString = LocalTime.parse("14:30:00");

Reading and Manipulating #

LocalTime t = LocalTime.of(14, 30, 45);

int hour    = t.getHour();   // 14
int minute  = t.getMinute(); // 30
int second  = t.getSecond(); // 45

// Add and subtract
LocalTime oneHourLater     = t.plusHours(1);    // 15:30:45
LocalTime thirtyMinAgo = t.minusMinutes(30); // 14:00:45

// LocalTime wraps around: past midnight it returns to the start of the day
LocalTime almostMidnight = LocalTime.of(23, 50);
LocalTime halfHourLater  = almostMidnight.plusMinutes(30); // 00:20 (wrap!)

// Comparing
boolean earlier = LocalTime.of(9, 0).isBefore(LocalTime.of(17, 0)); // true

LocalDateTime #

LocalDateTime is a combination of LocalDate and LocalTime in one object. It represents a specific moment — for example “Monday, August 17, 2025 at 8:00 AM” — but without time zone information. This is enough for most internal application needs that don’t manage multiple time zones.

Creating a LocalDateTime #

import java.time.LocalDateTime;

// Now
LocalDateTime now = LocalDateTime.now();

// Specific
LocalDateTime event = LocalDateTime.of(2025, 8, 17, 8, 0, 0);

// Combine from LocalDate + LocalTime
LocalDate date = LocalDate.of(2025, 8, 17);
LocalTime time   = LocalTime.of(8, 0);
LocalDateTime combined = LocalDateTime.of(date, time);
// or: date.atTime(time)
// or: date.atTime(8, 0)

// Parsing
LocalDateTime fromString = LocalDateTime.parse("2025-08-17T08:00:00");

Manipulation and Extraction #

LocalDateTime dt = LocalDateTime.of(2025, 8, 17, 14, 30);

// All plus/minus operations from LocalDate and LocalTime are available
LocalDateTime tomorrow   = dt.plusDays(1);    // 2025-08-18T14:30
LocalDateTime hourAgo = dt.minusHours(1); // 2025-08-17T13:30

// Extract components
LocalDate datePart = dt.toLocalDate(); // 2025-08-17
LocalTime timePart   = dt.toLocalTime(); // 14:30

// Replace specific components
LocalDateTime changeMonth = dt.withMonth(12).withDayOfMonth(31);
// 2025-12-31T14:30

Converting to ZonedDateTime #

import java.time.ZoneId;

LocalDateTime dt = LocalDateTime.of(2025, 8, 17, 14, 30);

// Pair it with a time zone to get a ZonedDateTime
ZonedDateTime inJakarta = dt.atZone(ZoneId.of("Asia/Jakarta"));
ZonedDateTime inLondon  = dt.atZone(ZoneId.of("Europe/London"));

ZonedDateTime #

ZonedDateTime is a LocalDateTime augmented with time zone information. Use it when your application operates across more than one time zone — reservation systems, global meeting apps, scheduled cross-country notifications.

Creating a ZonedDateTime #

import java.time.ZonedDateTime;
import java.time.ZoneId;

// Now in the system time zone
ZonedDateTime now = ZonedDateTime.now();

// Now in a specific time zone
ZonedDateTime inJakarta = ZonedDateTime.now(ZoneId.of("Asia/Jakarta"));
ZonedDateTime inTokyo   = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime inNY      = ZonedDateTime.now(ZoneId.of("America/New_York"));

// Specific
ZonedDateTime meeting = ZonedDateTime.of(
    2025, 8, 17, 14, 0, 0, 0,
    ZoneId.of("Asia/Jakarta")
);

// List all available zone IDs
ZoneId.getAvailableZoneIds().stream()
    .filter(z -> z.startsWith("Asia"))
    .sorted()
    .forEach(System.out::println);

Converting Between Time Zones #

ZonedDateTime jakartaTime = ZonedDateTime.of(
    2025, 8, 17, 14, 0, 0, 0,
    ZoneId.of("Asia/Jakarta")
);
// Asia/Jakarta = UTC+7

// Convert to another zone — the same moment, a different representation
ZonedDateTime londonTime = jakartaTime.withZoneSameInstant(ZoneId.of("Europe/London"));
ZonedDateTime tokyoTime  = jakartaTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));

System.out.println("Jakarta: " + jakartaTime); // 2025-08-17T14:00+07:00[Asia/Jakarta]
System.out.println("London:  " + londonTime);  // 2025-08-17T08:00+01:00[Europe/London]
System.out.println("Tokyo:   " + tokyoTime);   // 2025-08-17T16:00+09:00[Asia/Tokyo]

Offset and Zone Information #

ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Jakarta"));

ZoneId   zone   = zdt.getZone();   // Asia/Jakarta
ZoneOffset offset = zdt.getOffset(); // +07:00

// Convert to Instant (UTC)
Instant utc = zdt.toInstant();

// Convert to LocalDateTime (discard zone info)
LocalDateTime local = zdt.toLocalDateTime();

Instant #

Instant represents a single point in time on the universal timeline — counted in seconds and nanoseconds since the Unix epoch (January 1, 1970 00:00:00 UTC). There’s no concept of “date” or “hour” here, just a number. Use Instant for log timestamps, audit trails, measuring execution duration, or storing times in a database in UTC format.

Creating and Reading an Instant #

import java.time.Instant;

// Now in UTC
Instant now = Instant.now();
System.out.println(now); // 2025-08-17T07:00:00.123Z

// From epoch seconds
Instant fromEpoch = Instant.ofEpochSecond(1_700_000_000L);
Instant fromMillis = Instant.ofEpochMilli(1_700_000_000_000L);

// Epoch seconds and millis from an Instant
long epochSecond = now.getEpochSecond();
long epochMillis  = now.toEpochMilli();

// Boundary values
Instant dawnOfTime = Instant.EPOCH; // 1970-01-01T00:00:00Z
Instant minMax    = Instant.MIN;   // far in the past

Manipulation and Conversion #

Instant t = Instant.now();

// Add and subtract — only in seconds/nanoseconds/Duration units
Instant oneMinuteLater = t.plusSeconds(60);
Instant oneHourAgo   = t.minusSeconds(3600);
Instant withDuration = t.plus(Duration.ofHours(2));

// Convert an Instant to a ZonedDateTime for display purposes
ZonedDateTime display = t.atZone(ZoneId.of("Asia/Jakarta"));
System.out.println(display); // 2025-08-17T14:00:00.123+07:00[Asia/Jakarta]

// Comparing
Instant a = Instant.ofEpochSecond(1000);
Instant b = Instant.ofEpochSecond(2000);
boolean aEarlier = a.isBefore(b); // true
long diffSeconds  = b.getEpochSecond() - a.getEpochSecond(); // 1000

Duration and Period #

Java has two classes for representing time intervals: Duration for time-based intervals (hours, minutes, seconds), and Period for calendar-based intervals (days, months, years). They can’t be interchanged — choose based on context.

Duration — Time-Based Intervals #

Duration fits measuring how long something lasts: a process’s duration, gaps between events, or connection timeouts.

import java.time.Duration;

// Create a Duration from units
Duration twoHours        = Duration.ofHours(2);
Duration thirtyMin  = Duration.ofMinutes(30);
Duration hundredSeconds  = Duration.ofSeconds(100);

// Difference between two times
LocalTime start  = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration work   = Duration.between(start, end); // PT8H30M

System.out.println(work.toHours());   // 8
System.out.println(work.toMinutes()); // 510
System.out.println(work.toSeconds()); // 30600

// Duration between two Instants (for measuring execution)
Instant startTime = Instant.now();
// ... code being measured ...
Instant endTime = Instant.now();
Duration execution = Duration.between(startTime, endTime);
System.out.println("Execution time: " + execution.toMillis() + " ms");

// Manipulation
Duration extended = work.plusHours(1);   // PT9H30M
Duration shortened = work.minusMinutes(30); // PT8H
Duration doubled   = work.multipliedBy(2); // PT17H

Period — Calendar-Based Intervals #

Period fits differences between dates that need to account for days in a month and days in a year — like a person’s age, a contract period, or a due date.

import java.time.Period;

// Create a Period from units
Period oneMonth    = Period.ofMonths(1);
Period oneYear = Period.ofYears(1);
Period twoAndHalf = Period.of(2, 6, 0); // 2 years 6 months

// Difference between two dates
LocalDate birth   = LocalDate.of(1995, 7, 20);
LocalDate now = LocalDate.now();
Period age = Period.between(birth, now);

System.out.printf("Age: %d years %d months %d days%n",
    age.getYears(), age.getMonths(), age.getDays());

// Add a Period to a date
LocalDate contractStart  = LocalDate.of(2025, 1, 1);
LocalDate contractEnd  = contractStart.plus(Period.ofYears(2)); // 2027-01-01
LocalDate extension  = contractEnd.plus(Period.ofMonths(6)); // 2027-07-01

// Why Duration can't do this?
// Duration.ofDays(365) is not the same as Period.ofYears(1) in a leap year
LocalDate leapYear = LocalDate.of(2024, 1, 1);
LocalDate oneYearLater  = leapYear.plus(Period.ofYears(1)); // 2025-01-01 ✓
// Duration doesn't know about leap years — Period understands calendar context

Formatting and Parsing #

DateTimeFormatter is the tool for converting datetime objects into strings (formatting) and strings into datetime objects (parsing). It’s thread-safe and immutable, unlike the old SimpleDateFormat, which is notorious for not being thread-safe.

Built-in Formats #

import java.time.format.DateTimeFormatter;

LocalDateTime dt = LocalDateTime.of(2025, 8, 17, 14, 30, 0);

// Built-in ISO formats (no formatter needed)
System.out.println(dt);                                    // 2025-08-17T14:30
System.out.println(dt.format(DateTimeFormatter.ISO_DATE)); // 2025-08-17
System.out.println(dt.format(DateTimeFormatter.ISO_TIME)); // 14:30

// Local format
System.out.println(dt.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 2025-08-17T14:30:00

Custom Formats #

// Create a formatter with a custom pattern
DateTimeFormatter indonesian  = DateTimeFormatter.ofPattern("dd/MM/yyyy");
DateTimeFormatter full    = DateTimeFormatter.ofPattern("dd MMMM yyyy HH:mm");
DateTimeFormatter withDay = DateTimeFormatter.ofPattern("EEEE, dd MMMM yyyy", new java.util.Locale("id", "ID"));

LocalDate date = LocalDate.of(2025, 8, 17);
LocalDateTime time = LocalDateTime.of(2025, 8, 17, 8, 0);

System.out.println(date.format(indonesian));   // 17/08/2025
System.out.println(time.format(full));        // 17 August 2025 08:00
System.out.println(date.format(withDay));   // Minggu, 17 Agustus 2025

Parsing Strings into DateTime Objects #

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

// ANTI-PATTERN: not handling DateTimeParseException
LocalDateTime result = LocalDateTime.parse("17/08/2025 14:30", fmt); // can throw an exception

// CORRECT: handle the exception for external input
import java.time.format.DateTimeParseException;

String input = "17/08/2025 14:30";
try {
    LocalDateTime parsed = LocalDateTime.parse(input, fmt);
    System.out.println("Success: " + parsed); // 2025-08-17T14:30
} catch (DateTimeParseException e) {
    System.out.println("Invalid format: " + e.getMessage());
}

// Parsing various formats
LocalDate date = LocalDate.parse("2025-08-17"); // ISO default, no formatter needed
LocalTime time   = LocalTime.parse("14:30:00");   // ISO default

Frequently Used Format Patterns #

SymbolMeaningExample
yyyy4-digit year2025
MM2-digit month08
MMMMFull month nameAugust
dd2-digit day17
EEEEFull day nameSunday
HHHour (0–23)14
hhHour (1–12)02
mmMinute30
ssSecond00
aAM/PMPM
zTime zone nameWIB
ZZone offset+0700

Real-World Cases #

Calculating Age #

public static String calculateAge(LocalDate birthDate) {
    LocalDate now = LocalDate.now();
    Period age = Period.between(birthDate, now);
    return String.format("%d years %d months %d days",
        age.getYears(), age.getMonths(), age.getDays());
}

System.out.println(calculateAge(LocalDate.of(1995, 7, 20)));

Checking Whether Within Business Hours #

public static boolean isWithinBusinessHours(LocalTime open, LocalTime close) {
    LocalTime now = LocalTime.now();
    return !now.isBefore(open) && now.isBefore(close);
}

boolean open = isWithinBusinessHours(LocalTime.of(9, 0), LocalTime.of(17, 0));
System.out.println("Store open: " + open);

Measuring Execution Duration #

Instant start = Instant.now();

// ... process to measure ...
Thread.sleep(1500);

Instant end = Instant.now();
Duration duration = Duration.between(start, end);

System.out.printf("Finished in %d seconds %d ms%n",
    duration.toSecondsPart(),
    duration.toMillisPart());

Converting Database Timestamps to Local Display #

// Scenario: the database stores times in UTC (Instant)
Instant dbTimestamp = Instant.ofEpochSecond(1_724_000_000L);

// Display it to users in Jakarta
ZoneId jakartaZone = ZoneId.of("Asia/Jakarta");
ZonedDateTime showJakarta = dbTimestamp.atZone(jakartaZone);

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMM yyyy, HH:mm z");
System.out.println(showJakarta.format(fmt));
// Example output: 18 Aug 2024, 21:33 WIB

When to Use Each Class #

Use LOCALDATE when:
  ✓ Storing birth dates, transaction dates, deadlines
  ✓ No hour information needed at all
  ✓ Calculating day/month/year differences between dates

Use LOCALTIME when:
  ✓ Storing opening/closing hours, daily schedules, alarms
  ✓ No date information needed at all

Use LOCALDATETIME when:
  ✓ You need date AND time, but the app is in a single time zone
  ✓ Internal system data that doesn't need zone conversion

Use ZONEDDATETIME when:
  ✓ The app operates across more than one country/time zone
  ✓ Scheduled notifications, global meetings, cross-country reservations
  ✓ You need to convert display times based on the user's location

Use INSTANT when:
  ✓ Storing log timestamps, audit trails, event sourcing
  ✓ Storing to a database in UTC format
  ✓ Measuring code execution duration

Use DURATION when:
  ✓ Measuring time differences in hours/minutes/seconds
  ✓ Timeouts, gaps between events, process durations

Use PERIOD when:
  ✓ Measuring calendar differences in days/months/years
  ✓ Calculating age, contract periods, due dates

Summary #

  • Use java.time, not Date or Calendar — the old API is mutable and not thread-safe. java.time is immutable, thread-safe, and far more intuitive since Java 8.
  • All java.time objects are immutableplusDays(), minusHours(), and the like always return a new object. The original object never changes.
  • LocalDate for dates, LocalTime for times — don’t use LocalDateTime when you only need one of them.
  • ZonedDateTime when there are multiple time zonesLocalDateTime doesn’t store zone info. If users in different zones read the same data, the displayed time can differ from what was intended.
  • Instant for universal timestamps — store times in the database as Instant (UTC), convert to ZonedDateTime when displaying to users based on their zone.
  • Duration vs PeriodDuration is time-based (seconds/hours), Period is calendar-based (days/months/years). They can’t be interchanged — Duration.ofDays(365) is not the same as Period.ofYears(1) in a leap year.
  • DateTimeFormatter is thread-safe — it can be made a static final constant and shared from many threads. Unlike the old SimpleDateFormat, which must be recreated every time.
  • Always handle DateTimeParseException — when parsing external input (forms, APIs, files), always wrap it in a try-catch because the format may be invalid.

← Previous: Map   Next: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact