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.
| Class | Represents | Example |
|---|---|---|
LocalDate | Date only, without time | 2025-08-17 |
LocalTime | Time only, without date | 14:30:00 |
LocalDateTime | Date + time, without zone | 2025-08-17T14:30:00 |
ZonedDateTime | Date + time + time zone | 2025-08-17T14:30:00+07:00[Asia/Jakarta] |
Instant | A point in time in UTC (epoch) | 2025-08-17T07:30:00Z |
Duration | Time difference in hours/minutes/seconds | PT4H30M (4 hours 30 minutes) |
Period | Date difference in days/months/years | P1Y2M3D (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)"]Avoidjava.util.Date,java.util.Calendar, andjava.text.SimpleDateFormatin new code. All three are mutable, not thread-safe, and full of traps.java.timeis 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 #
| Symbol | Meaning | Example |
|---|---|---|
yyyy | 4-digit year | 2025 |
MM | 2-digit month | 08 |
MMMM | Full month name | August |
dd | 2-digit day | 17 |
EEEE | Full day name | Sunday |
HH | Hour (0–23) | 14 |
hh | Hour (1–12) | 02 |
mm | Minute | 30 |
ss | Second | 00 |
a | AM/PM | PM |
z | Time zone name | WIB |
Z | Zone 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, notDateorCalendar— the old API is mutable and not thread-safe.java.timeis immutable, thread-safe, and far more intuitive since Java 8.- All
java.timeobjects are immutable —plusDays(),minusHours(), and the like always return a new object. The original object never changes.LocalDatefor dates,LocalTimefor times — don’t useLocalDateTimewhen you only need one of them.ZonedDateTimewhen there are multiple time zones —LocalDateTimedoesn’t store zone info. If users in different zones read the same data, the displayed time can differ from what was intended.Instantfor universal timestamps — store times in the database asInstant(UTC), convert toZonedDateTimewhen displaying to users based on their zone.DurationvsPeriod—Durationis time-based (seconds/hours),Periodis calendar-based (days/months/years). They can’t be interchanged —Duration.ofDays(365)is not the same asPeriod.ofYears(1)in a leap year.DateTimeFormatteris thread-safe — it can be made astatic finalconstant and shared from many threads. Unlike the oldSimpleDateFormat, 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.