Date/Time API (java.time)
Working with dates and times is a common task in almost every application, but Java’s original Date and Calendar classes were clunky, mutable, and notoriously hard to use correctly. Java 8 introduced the java.time package — a clean, immutable, thread-safe API inspired by the Joda-Time library that finally makes date/time work feel natural.
Why the Old API Was Broken
Before Java 8, you had java.util.Date and java.util.Calendar. They had serious problems:
Datewas mutable — any method could silently change it.- Months were zero-indexed in
Calendar(Calendar.JANUARY == 0), causing endless bugs. - Neither class was thread-safe.
- Time zones were painful to deal with.
- No concept of “just a date” vs “just a time” vs “a date with time”.
Note:
java.util.DateandCalendarstill exist for backward compatibility, but you should usejava.timefor all new code.
Core Classes at a Glance
The java.time package splits the concept of “date/time” into focused, single-purpose classes:
| Class | What it represents |
|---|---|
LocalDate | A date (year, month, day) — no time, no time zone |
LocalTime | A time of day — no date, no time zone |
LocalDateTime | Date + time — no time zone |
ZonedDateTime | Date + time + time zone |
Instant | A point in time (UTC epoch-based timestamp) |
Duration | Amount of time in seconds/nanoseconds |
Period | Amount of time in years/months/days |
ZoneId | A time zone identifier (e.g., "America/New_York") |
DateTimeFormatter | Parsing and formatting date/time values |
All these classes are immutable. Operations like plusDays() return a new object — they never modify the original.
LocalDate
LocalDate represents a calendar date with no time component — perfect for birthdays, deadlines, and calendar events.
import java.time.LocalDate;
import java.time.Month;
public class LocalDateDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate christmas = LocalDate.of(2025, Month.DECEMBER, 25);
LocalDate fromString = LocalDate.parse("2025-07-04");
System.out.println("Today: " + today);
System.out.println("Christmas: " + christmas);
System.out.println("Independence Day: " + fromString);
System.out.println("Day of week: " + today.getDayOfWeek());
System.out.println("Is leap year: " + today.isLeapYear());
LocalDate nextWeek = today.plusWeeks(1);
System.out.println("Next week: " + nextWeek);
}
}
Output:
Today: 2026-06-13
Christmas: 2025-12-25
Independence Day: 2025-07-04
Day of week: SATURDAY
Is leap year: false
Next week: 2026-06-20
LocalTime
LocalTime represents a time of day, without any date or time zone.
import java.time.LocalTime;
public class LocalTimeDemo {
public static void main(String[] args) {
LocalTime now = LocalTime.now();
LocalTime meeting = LocalTime.of(14, 30, 0); // 2:30 PM
System.out.println("Current time: " + now);
System.out.println("Meeting at: " + meeting);
System.out.println("Hour: " + now.getHour());
System.out.println("Is before meeting: " + now.isBefore(meeting));
LocalTime oneHourLater = meeting.plusHours(1);
System.out.println("Meeting ends: " + oneHourLater);
}
}
Output:
Current time: 09:15:42.123
Meeting at: 14:30
Hour: 9
Is before meeting: true
Meeting ends: 15:30
LocalDateTime
LocalDateTime combines date and time — useful when you need both but don’t care about time zones (like logging events in the same region).
import java.time.LocalDateTime;
import java.time.Month;
public class LocalDateTimeDemo {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
LocalDateTime event = LocalDateTime.of(2025, Month.DECEMBER, 31, 23, 59, 59);
System.out.println("Now: " + now);
System.out.println("New Year's Eve: " + event);
System.out.println("Year: " + now.getYear());
System.out.println("Month: " + now.getMonth());
}
}
Output:
Now: 2026-06-13T09:15:42.123
New Year's Eve: 2025-12-31T23:59:59
Year: 2026
Month: JUNE
ZonedDateTime and ZoneId
When you need true time zone awareness — for international apps, scheduling, or storing timestamps — use ZonedDateTime.
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class ZonedDemo {
public static void main(String[] args) {
ZonedDateTime nowInUTC = ZonedDateTime.now(ZoneId.of("UTC"));
ZonedDateTime nowInNY = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime nowInTokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("UTC: " + nowInUTC);
System.out.println("New York: " + nowInNY);
System.out.println("Tokyo: " + nowInTokyo);
// Convert between zones
ZonedDateTime tokyoTime = nowInNY.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("NY time in Tokyo: " + tokyoTime);
}
}
Tip: Use
ZoneId.getAvailableZoneIds()to get a full list of valid zone IDs. Always prefer IANA zone names like"Europe/London"over short abbreviations like"EST", which can be ambiguous.
Instant
Instant is the machine-friendly time representation — a point on the UTC timeline measured in seconds and nanoseconds from the Unix epoch (1970-01-01T00:00:00Z). It’s ideal for storing timestamps in databases.
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class InstantDemo {
public static void main(String[] args) {
Instant now = Instant.now();
Instant future = now.plus(5, ChronoUnit.HOURS);
System.out.println("Now (epoch seconds): " + now.getEpochSecond());
System.out.println("Now: " + now);
System.out.println("5 hours later: " + future);
System.out.println("Is before future: " + now.isBefore(future));
}
}
Output:
Now (epoch seconds): 1749811200
Now: 2026-06-13T09:00:00Z
5 hours later: 2026-06-13T14:00:00Z
Is before future: true
Duration and Period
Duration measures time in hours, minutes, seconds, and nanoseconds. Period measures time in years, months, and days.
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
public class DurationPeriodDemo {
public static void main(String[] args) {
// Duration — for time-based amounts
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration workDay = Duration.between(start, end);
System.out.println("Work day: " + workDay.toHours() + " hours " + workDay.toMinutesPart() + " minutes");
// Period — for date-based amounts
LocalDate birthday = LocalDate.of(1995, 8, 15);
LocalDate today = LocalDate.of(2026, 6, 13);
Period age = Period.between(birthday, today);
System.out.println("Age: " + age.getYears() + " years, " + age.getMonths() + " months");
// Explicit creation
Duration twoHours = Duration.ofHours(2);
Period threeMonths = Period.ofMonths(3);
System.out.println("Two hours in seconds: " + twoHours.getSeconds());
System.out.println("Three months: " + threeMonths);
}
}
Output:
Work day: 8 hours 30 minutes
Age: 30 years, 9 months
Two hours in seconds: 7200
Three months: P3M
Note:
toMinutesPart()was added in Java 9. On Java 8, useworkDay.toMinutes() % 60instead.
DateTimeFormatter
You’ll often need to parse date strings from user input or external systems, and format dates for display. DateTimeFormatter handles both.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FormatterDemo {
public static void main(String[] args) {
// Predefined formatters
LocalDate date = LocalDate.now();
System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE));
// Custom pattern
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd MMM yyyy");
System.out.println(date.format(custom));
// Parsing a string
LocalDate parsed = LocalDate.parse("25/12/2025", DateTimeFormatter.ofPattern("dd/MM/yyyy"));
System.out.println("Parsed: " + parsed);
// Format LocalDateTime
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println("Formatted: " + now.format(dtFormatter));
}
}
Output:
2026-06-13
13 Jun 2026
Parsed: 2025-12-25
Formatted: 2026-06-13 09:15:42
Common pattern letters:
| Symbol | Meaning | Example |
|---|---|---|
yyyy | 4-digit year | 2026 |
MM | 2-digit month | 06 |
MMM | Short month name | Jun |
dd | 2-digit day | 13 |
HH | Hour (24h) | 14 |
hh | Hour (12h) | 02 |
mm | Minutes | 30 |
ss | Seconds | 45 |
a | AM/PM | PM |
z | Time zone name | UTC |
Comparing and Querying Dates
import java.time.LocalDate;
public class CompareDemo {
public static void main(String[] args) {
LocalDate date1 = LocalDate.of(2025, 1, 15);
LocalDate date2 = LocalDate.of(2025, 6, 20);
System.out.println("isBefore: " + date1.isBefore(date2)); // true
System.out.println("isAfter: " + date1.isAfter(date2)); // false
System.out.println("isEqual: " + date1.isEqual(date2)); // false
// Days between two dates
long daysBetween = date1.until(date2, java.time.temporal.ChronoUnit.DAYS);
System.out.println("Days between: " + daysBetween);
}
}
Output:
isBefore: true
isAfter: false
isEqual: false
Days between: 156
Interoperability with Legacy Code
You may need to convert between java.time and the old java.util.Date when working with legacy APIs or JDBC drivers.
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
public class LegacyConversionDemo {
public static void main(String[] args) {
// java.util.Date → Instant
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
System.out.println("Instant: " + instant);
// Instant → LocalDateTime
LocalDateTime ldt = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
System.out.println("LocalDateTime: " + ldt);
// LocalDateTime → java.util.Date
Date backToLegacy = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
System.out.println("Back to Date: " + backToLegacy);
}
}
Tip: When using JDBC with modern drivers (JDBC 4.2+), you can pass
LocalDateandLocalDateTimedirectly viaPreparedStatement.setObject()— no conversion needed.
Under the Hood
Immutability and thread safety. Every java.time class is declared final and stores all fields as private final. This means instances can be freely shared across threads without synchronization — a huge advantage over Calendar.
Value-based semantics. LocalDate, LocalTime, and friends are conceptually value types (similar to how int works). You should never use == to compare them — always use .equals() or the isBefore()/isAfter() methods. Java 21’s records and future value types (Project Valhalla) follow the same philosophy.
Instant vs LocalDateTime storage. An Instant is stored as two long fields: epochSecond and nanos. A LocalDateTime is stored as a LocalDate (packed into one long as year/month/day) and a LocalTime (stored as nanoseconds since midnight). Neither stores a time zone, which is why converting between them always requires a ZoneId.
Temporal arithmetic. Methods like plusDays() and minusMonths() go through the Temporal interface and TemporalUnit/TemporalAmount abstractions. This design lets ChronoUnit (e.g., ChronoUnit.WEEKS) and Period both plug in — a textbook use of interfaces.
Formatting performance. DateTimeFormatter instances are thread-safe and expensive to create. Always store them as static final constants rather than creating them inside loops or methods.
// Good practice
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
Related Topics
- Lambda Expressions —
java.timeAPIs work beautifully with lambdas and streams for filtering and transforming dates - Stream API — combine with streams to process collections of dates efficiently
- Optional — date parsing can fail;
Optionalis the modern way to handle absent values safely - Formatting Dates & Times — locale-aware formatting for internationalized applications
- Java 8 Features — the full picture of what arrived in Java 8 alongside
java.time - Records — immutable data classes, sharing the same design philosophy as
java.time