If you have ever dealt with dates in Java using java.util.Date or java.util.Calendar, you know the pain: mutable objects that change unexpectedly, months starting from 0, thread-safety issues, and a confusing API that mixes date concepts with time concepts. For over a decade, Java developers relied on third-party libraries like Joda-Time just to do basic date arithmetic correctly.
Java 8 fixed this by introducing the java.time package (JSR 310), designed by the creator of Joda-Time himself. This package is now the standard for all date and time work in Java.
| Problem with Legacy API | How java.time Fixes It |
|---|---|
Date is mutable — anyone can call setTime() and change it |
All java.time classes are immutable — methods return new instances |
| Months are 0-indexed (January = 0) | Months are 1-indexed (January = 1) or use the Month enum |
Date represents both date and time, but poorly |
Separate classes: LocalDate, LocalTime, LocalDateTime |
No built-in time zone handling beyond TimeZone |
ZonedDateTime and ZoneId with IANA time zone database |
Not thread-safe — SimpleDateFormat causes race conditions |
DateTimeFormatter is thread-safe and immutable |
| No concept of periods or durations | Period (date-based) and Duration (time-based) |
Confusing API (getYear() returns year minus 1900) |
Clear, consistent method naming: getYear() returns the actual year |
| Class | What It Represents | Example |
|---|---|---|
LocalDate |
Date only (no time, no zone) | 2026-02-28 |
LocalTime |
Time only (no date, no zone) | 14:30:00 |
LocalDateTime |
Date and time (no zone) | 2026-02-28T14:30:00 |
ZonedDateTime |
Date, time, and time zone | 2026-02-28T14:30:00-05:00[America/New_York] |
Instant |
Machine timestamp (epoch seconds) | 2026-02-28T19:30:00Z |
Period |
Date-based amount (years, months, days) | 2 years, 3 months, 5 days |
Duration |
Time-based amount (hours, minutes, seconds) | 2 hours, 30 minutes |
DateTimeFormatter |
Parsing and formatting | “dd/MM/yyyy” pattern |
The rule of thumb is simple: use the most specific type that fits your need. If you only care about a date (birthdays, holidays), use LocalDate. If you need to coordinate across time zones (meeting schedulers, flight departures), use ZonedDateTime. If you need a machine-readable timestamp for logging or databases, use Instant.
LocalDate represents a date without a time component and without a time zone. It stores a year, month, and day. This is the class you reach for when you care about what day something happens but not what time: birthdays, holidays, due dates, hire dates.
There are three primary ways to create a LocalDate:
import java.time.LocalDate;
import java.time.Month;
public class LocalDateCreation {
public static void main(String[] args) {
// 1. Current date from the system clock
LocalDate today = LocalDate.now();
System.out.println("Today: " + today); // e.g., 2026-02-28
// 2. Specific date using of()
LocalDate christmas = LocalDate.of(2026, 12, 25);
System.out.println("Christmas: " + christmas); // 2026-12-25
// Using Month enum (more readable)
LocalDate newYear = LocalDate.of(2027, Month.JANUARY, 1);
System.out.println("New Year: " + newYear); // 2027-01-01
// 3. Parsing a string (ISO 8601 format: yyyy-MM-dd)
LocalDate parsed = LocalDate.parse("2026-07-04");
System.out.println("Parsed: " + parsed); // 2026-07-04
}
}
Once you have a LocalDate, you can extract every piece of information from it:
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
public class LocalDateComponents {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, Month.FEBRUARY, 28);
System.out.println("Year: " + date.getYear()); // 2026
System.out.println("Month (enum): " + date.getMonth()); // FEBRUARY
System.out.println("Month (int): " + date.getMonthValue()); // 2
System.out.println("Day of month: " + date.getDayOfMonth()); // 28
System.out.println("Day of week: " + date.getDayOfWeek()); // SATURDAY
System.out.println("Day of year: " + date.getDayOfYear()); // 59
System.out.println("Length of month: " + date.lengthOfMonth()); // 28
System.out.println("Length of year: " + date.lengthOfYear()); // 365
System.out.println("Is leap year: " + date.isLeapYear()); // false
}
}
Because LocalDate is immutable, every manipulation method returns a new LocalDate. The original is never modified.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class LocalDateManipulation {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 2, 28);
// Adding
System.out.println("Plus 1 day: " + today.plusDays(1)); // 2026-03-01
System.out.println("Plus 2 weeks: " + today.plusWeeks(2)); // 2026-03-14
System.out.println("Plus 1 month: " + today.plusMonths(1)); // 2026-03-28
System.out.println("Plus 1 year: " + today.plusYears(1)); // 2027-02-28
// Subtracting
System.out.println("Minus 10 days: " + today.minusDays(10)); // 2026-02-18
System.out.println("Minus 3 months:" + today.minusMonths(3)); // 2025-11-28
// Using ChronoUnit (flexible alternative)
System.out.println("Plus 100 days: " + today.plus(100, ChronoUnit.DAYS)); // 2026-06-09
// Replacing components with "with" methods
System.out.println("With year 2030: " + today.withYear(2030)); // 2030-02-28
System.out.println("With month 6: " + today.withMonth(6)); // 2026-06-28
System.out.println("With day 15: " + today.withDayOfMonth(15)); // 2026-02-15
// Important: the original date is unchanged (immutability)
System.out.println("Original: " + today); // 2026-02-28
}
}
import java.time.LocalDate;
public class LocalDateComparison {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 2, 28);
LocalDate tomorrow = LocalDate.of(2026, 3, 1);
LocalDate alsoToday = LocalDate.of(2026, 2, 28);
// isBefore, isAfter, isEqual
System.out.println(today.isBefore(tomorrow)); // true
System.out.println(today.isAfter(tomorrow)); // false
System.out.println(today.isEqual(alsoToday)); // true
// equals() works the same as isEqual() for LocalDate
System.out.println(today.equals(alsoToday)); // true
// compareTo() -- returns negative, zero, or positive
System.out.println(today.compareTo(tomorrow)); // negative (today < tomorrow)
// NEVER use == to compare dates (compares object references, not values)
// today == alsoToday --> false (different objects!)
}
}
A common real-world use case: calculating someone’s age from their birth date.
import java.time.LocalDate;
import java.time.Month;
import java.time.Period;
public class AgeCalculator {
public static int calculateAge(LocalDate birthDate) {
LocalDate today = LocalDate.now();
if (birthDate.isAfter(today)) {
throw new IllegalArgumentException("Birth date cannot be in the future");
}
return Period.between(birthDate, today).getYears();
}
public static LocalDate getNextBirthday(LocalDate birthDate) {
LocalDate today = LocalDate.now();
LocalDate nextBirthday = birthDate.withYear(today.getYear());
// If birthday already passed this year, use next year
if (nextBirthday.isBefore(today) || nextBirthday.isEqual(today)) {
nextBirthday = nextBirthday.plusYears(1);
}
return nextBirthday;
}
public static void main(String[] args) {
LocalDate birthDate = LocalDate.of(1990, Month.JULY, 15);
int age = calculateAge(birthDate);
System.out.println("Birth date: " + birthDate); // 1990-07-15
System.out.println("Age: " + age + " years"); // e.g., 35 years
LocalDate nextBirthday = getNextBirthday(birthDate);
long daysUntil = LocalDate.now().until(nextBirthday, java.time.temporal.ChronoUnit.DAYS);
System.out.println("Next birthday: " + nextBirthday); // e.g., 2026-07-15
System.out.println("Days until: " + daysUntil); // e.g., 137
}
}
LocalTime represents a time without a date and without a time zone. It stores hours, minutes, seconds, and nanoseconds. Use it when you care about what time something happens but not what day: store opening hours, alarm times, daily schedules.
import java.time.LocalTime;
public class LocalTimeCreation {
public static void main(String[] args) {
// Current time
LocalTime now = LocalTime.now();
System.out.println("Now: " + now); // e.g., 14:30:22.123456789
// Specific time (hour, minute)
LocalTime nineAM = LocalTime.of(9, 0);
System.out.println("9 AM: " + nineAM); // 09:00
// Hour, minute, second
LocalTime precise = LocalTime.of(14, 30, 45);
System.out.println("Precise: " + precise); // 14:30:45
// Hour, minute, second, nanosecond
LocalTime nano = LocalTime.of(14, 30, 45, 123456789);
System.out.println("Nano: " + nano); // 14:30:45.123456789
// Parsing (ISO format: HH:mm:ss)
LocalTime parsed = LocalTime.parse("08:15:30");
System.out.println("Parsed: " + parsed); // 08:15:30
// Special constants
System.out.println("Midnight: " + LocalTime.MIDNIGHT); // 00:00
System.out.println("Noon: " + LocalTime.NOON); // 12:00
System.out.println("Min: " + LocalTime.MIN); // 00:00
System.out.println("Max: " + LocalTime.MAX); // 23:59:59.999999999
}
}
import java.time.LocalTime;
public class LocalTimeComponents {
public static void main(String[] args) {
LocalTime time = LocalTime.of(14, 30, 45, 123456789);
System.out.println("Hour: " + time.getHour()); // 14
System.out.println("Minute: " + time.getMinute()); // 30
System.out.println("Second: " + time.getSecond()); // 45
System.out.println("Nano: " + time.getNano()); // 123456789
// Convert to seconds since midnight
System.out.println("Seconds of day: " + time.toSecondOfDay()); // 52245
// Convert to nanoseconds since midnight
System.out.println("Nanos of day: " + time.toNanoOfDay());
}
}
import java.time.LocalTime;
public class LocalTimeManipulation {
public static void main(String[] args) {
LocalTime time = LocalTime.of(10, 30);
// Adding
System.out.println("Plus 2 hours: " + time.plusHours(2)); // 12:30
System.out.println("Plus 45 minutes:" + time.plusMinutes(45)); // 11:15
System.out.println("Plus 30 seconds:" + time.plusSeconds(30)); // 10:30:30
// Subtracting
System.out.println("Minus 3 hours: " + time.minusHours(3)); // 07:30
// Time wraps around midnight
LocalTime lateNight = LocalTime.of(23, 30);
System.out.println("23:30 + 2 hours: " + lateNight.plusHours(2)); // 01:30
// Comparing
LocalTime morning = LocalTime.of(8, 0);
LocalTime evening = LocalTime.of(20, 0);
System.out.println(morning.isBefore(evening)); // true
System.out.println(morning.isAfter(evening)); // false
}
}
A utility to check if a given time falls within business hours and calculate time until closing.
import java.time.Duration;
import java.time.LocalTime;
public class BusinessHoursChecker {
private final LocalTime openTime;
private final LocalTime closeTime;
private final LocalTime lunchStart;
private final LocalTime lunchEnd;
public BusinessHoursChecker() {
this.openTime = LocalTime.of(9, 0); // 9:00 AM
this.closeTime = LocalTime.of(17, 0); // 5:00 PM
this.lunchStart = LocalTime.of(12, 0); // 12:00 PM
this.lunchEnd = LocalTime.of(13, 0); // 1:00 PM
}
public boolean isOpen(LocalTime time) {
boolean withinHours = !time.isBefore(openTime) && time.isBefore(closeTime);
boolean isLunchBreak = !time.isBefore(lunchStart) && time.isBefore(lunchEnd);
return withinHours && !isLunchBreak;
}
public String getStatus(LocalTime time) {
if (time.isBefore(openTime)) {
Duration untilOpen = Duration.between(time, openTime);
return "Closed. Opens in " + untilOpen.toMinutes() + " minutes.";
}
if (!time.isBefore(lunchStart) && time.isBefore(lunchEnd)) {
Duration untilReopen = Duration.between(time, lunchEnd);
return "Lunch break. Reopens in " + untilReopen.toMinutes() + " minutes.";
}
if (!time.isBefore(openTime) && time.isBefore(closeTime)) {
Duration untilClose = Duration.between(time, closeTime);
return "Open. Closes in " + untilClose.toHours() + "h " +
(untilClose.toMinutes() % 60) + "m.";
}
return "Closed for the day. Opens tomorrow at " + openTime;
}
public static void main(String[] args) {
BusinessHoursChecker checker = new BusinessHoursChecker();
LocalTime[] testTimes = {
LocalTime.of(7, 30), // Before opening
LocalTime.of(10, 15), // Morning hours
LocalTime.of(12, 30), // Lunch break
LocalTime.of(15, 45), // Afternoon hours
LocalTime.of(18, 0) // After closing
};
for (LocalTime time : testTimes) {
System.out.printf("%s -> Open: %-5s | %s%n",
time, checker.isOpen(time), checker.getStatus(time));
}
// Output:
// 07:30 -> Open: false | Closed. Opens in 90 minutes.
// 10:15 -> Open: true | Open. Closes in 6h 45m.
// 12:30 -> Open: false | Lunch break. Reopens in 30 minutes.
// 15:45 -> Open: true | Open. Closes in 1h 15m.
// 18:00 -> Open: false | Closed for the day. Opens tomorrow at 09:00
}
}
LocalDateTime combines LocalDate and LocalTime into one object. It represents a date-time without a time zone. Use it when you need both the date and the time but the time zone is either irrelevant or implied: appointment timestamps within a single-timezone application, database DATETIME columns, event start times.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
public class LocalDateTimeCreation {
public static void main(String[] args) {
// Current date and time
LocalDateTime now = LocalDateTime.now();
System.out.println("Now: " + now); // e.g., 2026-02-28T14:30:22.123
// Specific date and time
LocalDateTime meeting = LocalDateTime.of(2026, Month.MARCH, 15, 10, 30);
System.out.println("Meeting: " + meeting); // 2026-03-15T10:30
// With seconds
LocalDateTime precise = LocalDateTime.of(2026, 3, 15, 10, 30, 45);
System.out.println("Precise: " + precise); // 2026-03-15T10:30:45
// From existing LocalDate and LocalTime
LocalDate date = LocalDate.of(2026, 6, 15);
LocalTime time = LocalTime.of(14, 0);
LocalDateTime combined = LocalDateTime.of(date, time);
System.out.println("Combined: " + combined); // 2026-06-15T14:00
// Attach time to a date
LocalDateTime startOfDay = date.atStartOfDay();
System.out.println("Start of day: " + startOfDay); // 2026-06-15T00:00
LocalDateTime atTime = date.atTime(9, 30);
System.out.println("At 9:30: " + atTime); // 2026-06-15T09:30
// Parse from string
LocalDateTime parsed = LocalDateTime.parse("2026-03-15T10:30:00");
System.out.println("Parsed: " + parsed); // 2026-03-15T10:30
}
}
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class LocalDateTimeConversion {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 15, 10, 30, 45);
// Extract date and time components
LocalDate date = dateTime.toLocalDate();
LocalTime time = dateTime.toLocalTime();
System.out.println("Date part: " + date); // 2026-03-15
System.out.println("Time part: " + time); // 10:30:45
// Manipulation works the same way
LocalDateTime nextMonth = dateTime.plusMonths(1);
System.out.println("Next month: " + nextMonth); // 2026-04-15T10:30:45
LocalDateTime twoHoursLater = dateTime.plusHours(2);
System.out.println("+2 hours: " + twoHoursLater); // 2026-03-15T12:30:45
// Replace components
LocalDateTime newTime = dateTime.withHour(8).withMinute(0);
System.out.println("Changed to 8 AM: " + newTime); // 2026-03-15T08:00:45
}
}
A simple appointment system that checks for conflicts and validates time slots.
import java.time.LocalDateTime;
import java.time.Duration;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AppointmentScheduler {
static class Appointment {
String title;
LocalDateTime start;
LocalDateTime end;
Appointment(String title, LocalDateTime start, Duration duration) {
this.title = title;
this.start = start;
this.end = start.plus(duration);
}
boolean conflictsWith(Appointment other) {
return this.start.isBefore(other.end) && this.end.isAfter(other.start);
}
@Override
public String toString() {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("MMM dd, HH:mm");
return String.format("%s [%s - %s]", title, start.format(fmt), end.format(fmt));
}
}
private final List appointments = new ArrayList<>();
public boolean schedule(String title, LocalDateTime start, Duration duration) {
Appointment newAppt = new Appointment(title, start, duration);
// Check for conflicts with existing appointments
for (Appointment existing : appointments) {
if (existing.conflictsWith(newAppt)) {
System.out.println("CONFLICT: '" + title + "' overlaps with '" + existing.title + "'");
return false;
}
}
// Validate business hours (9 AM - 5 PM)
if (newAppt.start.getHour() < 9 || newAppt.end.getHour() > 17 ||
(newAppt.end.getHour() == 17 && newAppt.end.getMinute() > 0)) {
System.out.println("REJECTED: '" + title + "' falls outside business hours (9-5)");
return false;
}
appointments.add(newAppt);
System.out.println("BOOKED: " + newAppt);
return true;
}
public static void main(String[] args) {
AppointmentScheduler scheduler = new AppointmentScheduler();
LocalDateTime monday = LocalDateTime.of(2026, 3, 16, 9, 0);
scheduler.schedule("Team Standup", monday, Duration.ofMinutes(30));
// BOOKED: Team Standup [Mar 16, 09:00 - Mar 16, 09:30]
scheduler.schedule("Code Review", monday.plusHours(1), Duration.ofHours(1));
// BOOKED: Code Review [Mar 16, 10:00 - Mar 16, 11:00]
scheduler.schedule("Quick Chat", monday.plusMinutes(15), Duration.ofMinutes(20));
// CONFLICT: 'Quick Chat' overlaps with 'Team Standup'
scheduler.schedule("Late Meeting", monday.withHour(16), Duration.ofHours(2));
// REJECTED: 'Late Meeting' falls outside business hours (9-5)
scheduler.schedule("Lunch 1:1", monday.withHour(12), Duration.ofHours(1));
// BOOKED: Lunch 1:1 [Mar 16, 12:00 - Mar 16, 13:00]
}
}
ZonedDateTime is a date-time with a full time zone, such as America/New_York or Europe/London. This is the class you need whenever time zone matters: scheduling meetings across countries, flight departure/arrival times, coordinating deployments across data centers.
Java uses the IANA Time Zone Database (also known as the Olson database). Always use region-based zone IDs like America/New_York rather than abbreviations like EST, because abbreviations are ambiguous (CST could mean Central Standard Time or China Standard Time).
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
public class ZonedDateTimeCreation {
public static void main(String[] args) {
// Current date-time in the system's default zone
ZonedDateTime now = ZonedDateTime.now();
System.out.println("Now: " + now);
// e.g., 2026-02-28T14:30:00-05:00[America/New_York]
// Current date-time in a specific zone
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println("Tokyo: " + tokyo);
// From LocalDateTime + ZoneId
LocalDateTime ldt = LocalDateTime.of(2026, 3, 15, 10, 30);
ZonedDateTime newYork = ldt.atZone(ZoneId.of("America/New_York"));
System.out.println("New York: " + newYork);
// 2026-03-15T10:30-04:00[America/New_York] (EDT in March)
// Using of()
ZonedDateTime london = ZonedDateTime.of(2026, 3, 15, 10, 30, 0, 0,
ZoneId.of("Europe/London"));
System.out.println("London: " + london);
// Parse from string
ZonedDateTime parsed = ZonedDateTime.parse(
"2026-03-15T10:30:00-04:00[America/New_York]");
System.out.println("Parsed: " + parsed);
// Common zone IDs
System.out.println("\nDefault zone: " + ZoneId.systemDefault());
System.out.println("UTC: " + ZoneId.of("UTC"));
}
}
This is one of the most powerful features of ZonedDateTime: converting the same instant in time to different zones. The withZoneSameInstant() method changes the zone while keeping the same moment in time. The withZoneSameLocal() method keeps the local date-time and changes the zone (rarely what you want).
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class TimeZoneConversion {
public static void main(String[] args) {
// A meeting is at 10:00 AM New York time
ZonedDateTime newYork = ZonedDateTime.of(
LocalDateTime.of(2026, 6, 15, 10, 0),
ZoneId.of("America/New_York")
);
// What time is that in other cities?
ZonedDateTime london = newYork.withZoneSameInstant(ZoneId.of("Europe/London"));
ZonedDateTime tokyo = newYork.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
ZonedDateTime sydney = newYork.withZoneSameInstant(ZoneId.of("Australia/Sydney"));
ZonedDateTime losAngeles = newYork.withZoneSameInstant(ZoneId.of("America/Los_Angeles"));
System.out.println("Meeting time across zones:");
System.out.println("New York: " + newYork); // 10:00 EDT (-04:00)
System.out.println("London: " + london); // 15:00 BST (+01:00)
System.out.println("Tokyo: " + tokyo); // 23:00 JST (+09:00)
System.out.println("Sydney: " + sydney); // 00:00+1 AEST (+10:00)
System.out.println("Los Angeles: " + losAngeles); // 07:00 PDT (-07:00)
}
}
ZonedDateTime automatically handles daylight saving time (DST) transitions. This is critical for correctness — naive date-time calculations that ignore DST can be off by an hour.
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.Duration;
public class DaylightSavingDemo {
public static void main(String[] args) {
ZoneId eastern = ZoneId.of("America/New_York");
// Spring Forward: March 8, 2026 at 2:00 AM -> clocks jump to 3:00 AM
ZonedDateTime beforeSpringForward = ZonedDateTime.of(
LocalDateTime.of(2026, 3, 8, 1, 30), eastern);
ZonedDateTime afterSpringForward = beforeSpringForward.plusHours(1);
System.out.println("Before spring forward: " + beforeSpringForward);
// 2026-03-08T01:30-05:00[America/New_York]
System.out.println("After +1 hour: " + afterSpringForward);
// 2026-03-08T03:30-04:00[America/New_York] (skipped 2:30!)
// Duration correctly accounts for DST
Duration gap = Duration.between(beforeSpringForward, afterSpringForward);
System.out.println("Actual duration: " + gap); // PT1H (still 1 hour of real time)
// Fall Back: November 1, 2026 at 2:00 AM -> clocks fall back to 1:00 AM
ZonedDateTime beforeFallBack = ZonedDateTime.of(
LocalDateTime.of(2026, 11, 1, 0, 30), eastern);
ZonedDateTime afterFallBack = beforeFallBack.plusHours(2);
System.out.println("\nBefore fall back: " + beforeFallBack);
System.out.println("After +2 hours: " + afterFallBack);
}
}
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
public class MeetingScheduler {
static class Participant {
String name;
ZoneId zone;
LocalTime workStart;
LocalTime workEnd;
Participant(String name, String zoneId) {
this.name = name;
this.zone = ZoneId.of(zoneId);
this.workStart = LocalTime.of(9, 0);
this.workEnd = LocalTime.of(17, 0);
}
boolean isAvailable(ZonedDateTime meetingTime, Duration duration) {
ZonedDateTime localMeeting = meetingTime.withZoneSameInstant(zone);
LocalTime meetStart = localMeeting.toLocalTime();
LocalTime meetEnd = meetStart.plus(duration);
return !meetStart.isBefore(workStart) && !meetEnd.isAfter(workEnd);
}
}
public static void findMeetingTime(List participants,
LocalDate date, Duration duration) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("HH:mm z");
System.out.println("Finding " + duration.toMinutes() + "-minute slot on " + date);
System.out.println("---");
// Try every hour from 0:00 to 23:00 UTC
for (int hour = 0; hour < 24; hour++) {
ZonedDateTime candidate = ZonedDateTime.of(date, LocalTime.of(hour, 0),
ZoneId.of("UTC"));
boolean allAvailable = participants.stream()
.allMatch(p -> p.isAvailable(candidate, duration));
if (allAvailable) {
System.out.println("AVAILABLE SLOT:");
for (Participant p : participants) {
ZonedDateTime local = candidate.withZoneSameInstant(p.zone);
System.out.printf(" %-12s -> %s%n", p.name, local.format(fmt));
}
System.out.println();
}
}
}
public static void main(String[] args) {
List team = Arrays.asList(
new Participant("Alice", "America/New_York"),
new Participant("Bob", "Europe/London"),
new Participant("Chika", "Asia/Tokyo")
);
findMeetingTime(team, LocalDate.of(2026, 6, 15), Duration.ofHours(1));
// Output: Shows overlapping work hours across all three zones
// The only common slot is typically around 21:00-23:00 UTC
// (morning in Tokyo, afternoon in London, morning in New York)
}
}
Instant represents a single point on the timeline, measured as nanoseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC). It has no concept of human time zones, months, or days — it is purely a machine timestamp. Use it for logging, measuring elapsed time, recording when events occurred, and storing timestamps in databases.
import java.time.Instant;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class InstantDemo {
public static void main(String[] args) {
// Current instant (always in UTC)
Instant now = Instant.now();
System.out.println("Now: " + now);
// e.g., 2026-02-28T19:30:00.123456789Z (the Z means UTC)
// From epoch seconds
Instant epoch = Instant.EPOCH;
System.out.println("Epoch: " + epoch); // 1970-01-01T00:00:00Z
Instant fromSeconds = Instant.ofEpochSecond(1_000_000_000);
System.out.println("1 billion seconds: " + fromSeconds); // 2001-09-09T01:46:40Z
// From epoch milliseconds (common in legacy APIs and JavaScript)
Instant fromMillis = Instant.ofEpochMilli(System.currentTimeMillis());
System.out.println("From millis: " + fromMillis);
// Getting epoch values
System.out.println("Epoch seconds: " + now.getEpochSecond());
System.out.println("Nano adjustment: " + now.getNano());
// Arithmetic
Instant oneHourLater = now.plus(Duration.ofHours(1));
Instant yesterday = now.minus(Duration.ofDays(1));
// Duration between two instants
Duration between = Duration.between(yesterday, now);
System.out.println("Between: " + between); // PT24H
// Convert to ZonedDateTime for human-readable display
ZonedDateTime zdt = now.atZone(ZoneId.of("America/New_York"));
System.out.println("As New York time: " + zdt);
}
}
Instant is the ideal tool for benchmarking and measuring how long operations take.
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class ExecutionTimer {
@FunctionalInterface
interface Task {
void run();
}
public static Duration measure(Task task) {
Instant start = Instant.now();
task.run();
Instant end = Instant.now();
return Duration.between(start, end);
}
public static void main(String[] args) {
int size = 100_000;
// Measure ArrayList add performance
Duration arrayListTime = measure(() -> {
List list = new ArrayList<>();
for (int i = 0; i < size; i++) {
list.add(0, i); // Insert at beginning (worst case)
}
});
// Measure LinkedList add performance
Duration linkedListTime = measure(() -> {
List list = new LinkedList<>();
for (int i = 0; i < size; i++) {
list.add(0, i); // Insert at beginning (best case for linked list)
}
});
System.out.println("Inserting " + size + " elements at index 0:");
System.out.println("ArrayList: " + arrayListTime.toMillis() + " ms");
System.out.println("LinkedList: " + linkedListTime.toMillis() + " ms");
// Example output:
// ArrayList: 1245 ms
// LinkedList: 12 ms
}
}
Java provides two classes for representing amounts of time:
Period -- a date-based amount of time (years, months, days). Think: "2 years, 3 months, and 5 days"Duration -- a time-based amount of time (hours, minutes, seconds, nanoseconds). Think: "2 hours and 30 minutes"The distinction matters because date-based arithmetic and time-based arithmetic behave differently. Adding "1 month" to January 31 gives February 28 (or 29), but adding "30 days" gives March 2. These are different operations.
import java.time.LocalDate;
import java.time.Period;
public class PeriodDemo {
public static void main(String[] args) {
// Creating Periods
Period twoYears = Period.ofYears(2);
Period threeMonths = Period.ofMonths(3);
Period tenDays = Period.ofDays(10);
Period custom = Period.of(1, 6, 15); // 1 year, 6 months, 15 days
System.out.println("Custom: " + custom); // P1Y6M15D (ISO 8601)
// Period between two dates
LocalDate startDate = LocalDate.of(2024, 1, 1);
LocalDate endDate = LocalDate.of(2026, 3, 15);
Period between = Period.between(startDate, endDate);
System.out.println("Between: " + between); // P2Y2M14D
System.out.println("Years: " + between.getYears()); // 2
System.out.println("Months: " + between.getMonths()); // 2
System.out.println("Days: " + between.getDays()); // 14
System.out.println("Total months: " + between.toTotalMonths()); // 26
// Applying a Period to a date
LocalDate today = LocalDate.of(2026, 2, 28);
LocalDate future = today.plus(custom);
System.out.println("Today + " + custom + " = " + future);
// 2026-02-28 + P1Y6M15D = 2027-09-12
// Period arithmetic
Period doubled = custom.multipliedBy(2);
System.out.println("Doubled: " + doubled); // P2Y12M30D
Period negated = custom.negated();
System.out.println("Negated: " + negated); // P-1Y-6M-15D
// Checking if zero
System.out.println("Is zero: " + Period.ZERO.isZero()); // true
System.out.println("Is negative: " + negated.isNegative()); // true
}
}
import java.time.Duration;
import java.time.LocalTime;
import java.time.Instant;
public class DurationDemo {
public static void main(String[] args) {
// Creating Durations
Duration twoHours = Duration.ofHours(2);
Duration thirtyMinutes = Duration.ofMinutes(30);
Duration fiveSeconds = Duration.ofSeconds(5);
Duration halfSecond = Duration.ofMillis(500);
System.out.println("2 hours: " + twoHours); // PT2H
System.out.println("30 minutes: " + thirtyMinutes); // PT30M
System.out.println("5 seconds: " + fiveSeconds); // PT5S
// Duration between two times
LocalTime start = LocalTime.of(9, 0);
LocalTime end = LocalTime.of(17, 30);
Duration workDay = Duration.between(start, end);
System.out.println("Work day: " + workDay); // PT8H30M
System.out.println("Hours: " + workDay.toHours()); // 8
System.out.println("Minutes: " + workDay.toMinutes()); // 510
System.out.println("Seconds: " + workDay.getSeconds()); // 30600
// Java 9+ part extraction
System.out.println("Hours part: " + workDay.toHoursPart()); // 8
System.out.println("Minutes part: " + workDay.toMinutesPart()); // 30
// Duration between two Instants
Instant i1 = Instant.parse("2026-02-28T10:00:00Z");
Instant i2 = Instant.parse("2026-02-28T14:30:00Z");
Duration gap = Duration.between(i1, i2);
System.out.println("Gap: " + gap); // PT4H30M
// Arithmetic
Duration total = twoHours.plus(thirtyMinutes);
System.out.println("Total: " + total); // PT2H30M
Duration doubled = twoHours.multipliedBy(2);
System.out.println("Doubled: " + doubled); // PT4H
// Apply to time
LocalTime lunchEnd = LocalTime.of(13, 0);
LocalTime afternoonEnd = lunchEnd.plus(Duration.ofHours(4));
System.out.println("Afternoon ends: " + afternoonEnd); // 17:00
}
}
| Aspect | Period | Duration |
|---|---|---|
| Measures | Years, months, days | Hours, minutes, seconds, nanos |
| Works with | LocalDate, LocalDateTime |
LocalTime, LocalDateTime, Instant |
| Use case | Human calendar concepts | Precise time measurements |
| Example | "Renew subscription in 1 year" | "Session expires in 30 minutes" |
| ISO format | P1Y2M3D | PT2H30M15S |
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class SubscriptionManager {
enum Plan {
TRIAL(Period.ofDays(14)),
MONTHLY(Period.ofMonths(1)),
YEARLY(Period.ofYears(1));
final Period duration;
Plan(Period duration) { this.duration = duration; }
}
static class Subscription {
String user;
Plan plan;
LocalDate startDate;
LocalDate expiryDate;
Subscription(String user, Plan plan, LocalDate startDate) {
this.user = user;
this.plan = plan;
this.startDate = startDate;
this.expiryDate = startDate.plus(plan.duration);
}
boolean isActive(LocalDate today) {
return !today.isAfter(expiryDate);
}
long daysRemaining(LocalDate today) {
if (!isActive(today)) return 0;
return ChronoUnit.DAYS.between(today, expiryDate);
}
Subscription renew() {
return new Subscription(user, plan, expiryDate);
}
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 2, 28);
Subscription trial = new Subscription("Alice", Plan.TRIAL, today);
Subscription monthly = new Subscription("Bob", Plan.MONTHLY, today);
Subscription yearly = new Subscription("Charlie", Plan.YEARLY, today);
System.out.printf("%-10s | Plan: %-8s | Expires: %s | Days left: %d%n",
trial.user, trial.plan, trial.expiryDate, trial.daysRemaining(today));
System.out.printf("%-10s | Plan: %-8s | Expires: %s | Days left: %d%n",
monthly.user, monthly.plan, monthly.expiryDate, monthly.daysRemaining(today));
System.out.printf("%-10s | Plan: %-8s | Expires: %s | Days left: %d%n",
yearly.user, yearly.plan, yearly.expiryDate, yearly.daysRemaining(today));
// Output:
// Alice | Plan: TRIAL | Expires: 2026-03-14 | Days left: 14
// Bob | Plan: MONTHLY | Expires: 2026-03-28 | Days left: 28
// Charlie | Plan: YEARLY | Expires: 2027-02-28 | Days left: 365
// Renew Bob's subscription
Subscription renewed = monthly.renew();
System.out.println("\nBob renewed: expires " + renewed.expiryDate);
// Bob renewed: expires 2026-04-28
}
}
DateTimeFormatter is the java.time replacement for SimpleDateFormat. Unlike its predecessor, it is immutable and thread-safe -- you can safely share a single formatter instance across threads.
Java provides several built-in formatters for common ISO 8601 formats:
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class PredefinedFormatters {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 3, 15);
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 15, 14, 30, 0);
ZonedDateTime zdt = dateTime.atZone(ZoneId.of("America/New_York"));
// ISO formatters (most common for APIs and data exchange)
System.out.println(date.format(DateTimeFormatter.ISO_LOCAL_DATE));
// 2026-03-15
System.out.println(dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
// 2026-03-15T14:30:00
System.out.println(zdt.format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
// 2026-03-15T14:30:00-04:00[America/New_York]
System.out.println(date.format(DateTimeFormatter.ISO_ORDINAL_DATE));
// 2026-074 (day 74 of the year)
System.out.println(date.format(DateTimeFormatter.BASIC_ISO_DATE));
// 20260315
}
}
For human-readable formats, you define your own pattern using DateTimeFormatter.ofPattern(). Here are the most commonly used pattern symbols:
| Symbol | Meaning | Example |
|---|---|---|
yyyy |
4-digit year | 2026 |
yy |
2-digit year | 26 |
MM |
Month (01-12) | 03 |
MMM |
Abbreviated month | Mar |
MMMM |
Full month name | March |
dd |
Day of month (01-31) | 15 |
EEE |
Abbreviated day | Sun |
EEEE |
Full day name | Sunday |
HH |
Hour (00-23) | 14 |
hh |
Hour (01-12) | 02 |
mm |
Minute (00-59) | 30 |
ss |
Second (00-59) | 45 |
a |
AM/PM | PM |
z |
Time zone name | EDT |
Z |
Zone offset | -0400 |
VV |
Zone ID | America/New_York |
Critical warning: MM is months, mm is minutes. Mixing them up is one of the most common date/time bugs. The pattern "yyyy-mm-dd" gives you "2026-30-15" (using minutes instead of months).
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class CustomFormatting {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 15, 14, 30, 45);
// Common custom patterns
DateTimeFormatter f1 = DateTimeFormatter.ofPattern("dd/MM/yyyy");
System.out.println(dateTime.format(f1)); // 15/03/2026
DateTimeFormatter f2 = DateTimeFormatter.ofPattern("MM-dd-yyyy HH:mm");
System.out.println(dateTime.format(f2)); // 03-15-2026 14:30
DateTimeFormatter f3 = DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy");
System.out.println(dateTime.format(f3)); // Sunday, March 15, 2026
DateTimeFormatter f4 = DateTimeFormatter.ofPattern("hh:mm a");
System.out.println(dateTime.format(f4)); // 02:30 PM
DateTimeFormatter f5 = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
System.out.println(dateTime.format(f5)); // 2026/03/15 14:30:45
// Locale-specific formatting
DateTimeFormatter german = DateTimeFormatter.ofPattern(
"EEEE, dd. MMMM yyyy", Locale.GERMAN);
System.out.println(dateTime.format(german)); // Sonntag, 15. März 2026
DateTimeFormatter french = DateTimeFormatter.ofPattern(
"dd MMMM yyyy", Locale.FRENCH);
System.out.println(dateTime.format(french)); // 15 mars 2026
// With time zone
ZonedDateTime zdt = dateTime.atZone(ZoneId.of("America/New_York"));
DateTimeFormatter withZone = DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss z (VV)");
System.out.println(zdt.format(withZone));
// 2026-03-15 14:30:45 EDT (America/New_York)
}
}
Parsing is the reverse of formatting: converting a String into a date/time object. If the string does not match the expected format, a DateTimeParseException is thrown.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
public class ParsingDates {
public static void main(String[] args) {
// Parse ISO format (no formatter needed)
LocalDate date = LocalDate.parse("2026-03-15");
System.out.println("Parsed ISO: " + date); // 2026-03-15
// Parse custom format
DateTimeFormatter slashFormat = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate custom = LocalDate.parse("15/03/2026", slashFormat);
System.out.println("Parsed custom: " + custom); // 2026-03-15
DateTimeFormatter usFormat = DateTimeFormatter.ofPattern("MM-dd-yyyy");
LocalDate usDate = LocalDate.parse("03-15-2026", usFormat);
System.out.println("Parsed US: " + usDate); // 2026-03-15
DateTimeFormatter fullFormat = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
LocalDateTime dateTime = LocalDateTime.parse("2026/03/15 14:30:45", fullFormat);
System.out.println("Parsed datetime: " + dateTime); // 2026-03-15T14:30:45
// Always handle parse exceptions in production code
try {
LocalDate bad = LocalDate.parse("not-a-date");
} catch (DateTimeParseException e) {
System.out.println("Parse error: " + e.getMessage());
// Text 'not-a-date' could not be parsed at index 0
}
try {
// Wrong format: trying to parse US format with slash formatter
LocalDate wrong = LocalDate.parse("03-15-2026", slashFormat);
} catch (DateTimeParseException e) {
System.out.println("Format mismatch: " + e.getMessage());
}
}
}
In real applications, you frequently need to convert between the different date/time types. Here is a comprehensive reference for all common conversions.
import java.time.*;
public class TypeConversions {
public static void main(String[] args) {
// ============================================
// LocalDate <-> LocalDateTime
// ============================================
LocalDate date = LocalDate.of(2026, 3, 15);
// LocalDate -> LocalDateTime (add time)
LocalDateTime atStartOfDay = date.atStartOfDay(); // 2026-03-15T00:00
LocalDateTime atSpecificTime = date.atTime(14, 30); // 2026-03-15T14:30
LocalDateTime atLocalTime = date.atTime(LocalTime.NOON); // 2026-03-15T12:00
// LocalDateTime -> LocalDate (drop time)
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 15, 14, 30);
LocalDate dateOnly = dateTime.toLocalDate(); // 2026-03-15
LocalTime timeOnly = dateTime.toLocalTime(); // 14:30
// ============================================
// LocalDateTime <-> ZonedDateTime
// ============================================
// LocalDateTime -> ZonedDateTime (add zone)
ZonedDateTime zoned = dateTime.atZone(ZoneId.of("America/New_York"));
// 2026-03-15T14:30-04:00[America/New_York]
// ZonedDateTime -> LocalDateTime (drop zone)
LocalDateTime local = zoned.toLocalDateTime(); // 2026-03-15T14:30
// ZonedDateTime -> LocalDate / LocalTime
LocalDate zonedDate = zoned.toLocalDate(); // 2026-03-15
LocalTime zonedTime = zoned.toLocalTime(); // 14:30
// ============================================
// ZonedDateTime <-> Instant
// ============================================
// ZonedDateTime -> Instant (to UTC epoch)
Instant instant = zoned.toInstant();
System.out.println("Instant: " + instant);
// 2026-03-15T18:30:00Z (14:30 EDT = 18:30 UTC)
// Instant -> ZonedDateTime (add zone)
ZonedDateTime fromInstant = instant.atZone(ZoneId.of("Asia/Tokyo"));
System.out.println("Tokyo: " + fromInstant);
// 2026-03-16T03:30+09:00[Asia/Tokyo]
// ============================================
// Legacy Date <-> Modern Types
// ============================================
java.util.Date legacyDate = new java.util.Date();
// java.util.Date -> Instant
Instant fromLegacy = legacyDate.toInstant();
// Instant -> java.util.Date
java.util.Date backToLegacy = java.util.Date.from(fromLegacy);
// java.util.Date -> LocalDate (via Instant + zone)
LocalDate fromLegacyDate = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDate();
// LocalDate -> java.util.Date (via Instant + zone)
java.util.Date toLegacyDate = java.util.Date.from(
date.atStartOfDay(ZoneId.systemDefault()).toInstant());
// java.sql.Date <-> LocalDate (direct conversion)
java.sql.Date sqlDate = java.sql.Date.valueOf(date);
LocalDate fromSql = sqlDate.toLocalDate();
// java.sql.Timestamp <-> LocalDateTime
java.sql.Timestamp timestamp = java.sql.Timestamp.valueOf(dateTime);
LocalDateTime fromTimestamp = timestamp.toLocalDateTime();
System.out.println("All conversions successful!");
}
}
| From | To | Method |
|---|---|---|
LocalDate |
LocalDateTime |
date.atStartOfDay() or date.atTime(14, 30) |
LocalDateTime |
LocalDate |
dateTime.toLocalDate() |
LocalDateTime |
LocalTime |
dateTime.toLocalTime() |
LocalDateTime |
ZonedDateTime |
dateTime.atZone(zoneId) |
ZonedDateTime |
LocalDateTime |
zoned.toLocalDateTime() |
ZonedDateTime |
Instant |
zoned.toInstant() |
Instant |
ZonedDateTime |
instant.atZone(zoneId) |
java.util.Date |
Instant |
legacyDate.toInstant() |
Instant |
java.util.Date |
Date.from(instant) |
java.sql.Date |
LocalDate |
sqlDate.toLocalDate() |
LocalDate |
java.sql.Date |
java.sql.Date.valueOf(date) |
Here are the operations you will use most often in real-world Java applications. Each one comes up regularly in business logic, reporting, and scheduling systems.
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class MonthBoundaries {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 3, 15);
LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
System.out.println("Date: " + date); // 2026-03-15
System.out.println("First day: " + firstDay); // 2026-03-01
System.out.println("Last day: " + lastDay); // 2026-03-31
// First and last day of year
LocalDate firstOfYear = date.with(TemporalAdjusters.firstDayOfYear());
LocalDate lastOfYear = date.with(TemporalAdjusters.lastDayOfYear());
System.out.println("First of year: " + firstOfYear); // 2026-01-01
System.out.println("Last of year: " + lastOfYear); // 2026-12-31
// First day of next month
LocalDate firstOfNext = date.with(TemporalAdjusters.firstDayOfNextMonth());
System.out.println("First of next month: " + firstOfNext); // 2026-04-01
}
}
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class DayOfWeekOperations {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 2, 28); // Saturday
// Next Monday (strictly after the given date)
LocalDate nextMonday = date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
System.out.println("Next Monday: " + nextMonday); // 2026-03-02
// Next or same Monday (returns same date if it's already Monday)
LocalDate nextOrSame = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY));
System.out.println("Next or same Saturday: " + nextOrSame); // 2026-02-28 (already Sat)
// Previous Friday
LocalDate prevFriday = date.with(TemporalAdjusters.previous(DayOfWeek.FRIDAY));
System.out.println("Previous Friday: " + prevFriday); // 2026-02-27
// First Monday of the month
LocalDate firstMonday = date.with(
TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
System.out.println("First Monday: " + firstMonday); // 2026-02-02
// Last Friday of the month
LocalDate lastFriday = date.with(
TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("Last Friday: " + lastFriday); // 2026-02-27
}
}
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class BusinessDayCalculator {
public static boolean isWeekend(LocalDate date) {
DayOfWeek day = date.getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
}
public static boolean isBusinessDay(LocalDate date) {
return !isWeekend(date);
// In production, also check a holiday calendar
}
public static LocalDate addBusinessDays(LocalDate start, int days) {
LocalDate result = start;
int added = 0;
while (added < days) {
result = result.plusDays(1);
if (isBusinessDay(result)) {
added++;
}
}
return result;
}
public static long countBusinessDays(LocalDate start, LocalDate end) {
long count = 0;
LocalDate current = start;
while (!current.isAfter(end)) {
if (isBusinessDay(current)) {
count++;
}
current = current.plusDays(1);
}
return count;
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 2, 28); // Saturday
System.out.println(today + " is weekend: " + isWeekend(today)); // true
// Add 5 business days (skip weekends)
LocalDate deadline = addBusinessDays(today, 5);
System.out.println("5 business days from " + today + ": " + deadline);
// 2026-03-06 (Friday)
// Count business days in March 2026
LocalDate marchStart = LocalDate.of(2026, 3, 1);
LocalDate marchEnd = LocalDate.of(2026, 3, 31);
long businessDays = countBusinessDays(marchStart, marchEnd);
System.out.println("Business days in March 2026: " + businessDays); // 22
}
}
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DateRanges {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 3, 1);
LocalDate end = LocalDate.of(2026, 3, 7);
// Java 9+ datesUntil -- stream of dates from start (inclusive) to end (exclusive)
List week = start.datesUntil(end.plusDays(1))
.collect(Collectors.toList());
System.out.println("Week: " + week);
// [2026-03-01, 2026-03-02, ..., 2026-03-07]
// Every Monday in March 2026
LocalDate marchStart = LocalDate.of(2026, 3, 1);
LocalDate marchEnd = LocalDate.of(2026, 3, 31);
List mondays = marchStart.datesUntil(marchEnd.plusDays(1))
.filter(d -> d.getDayOfWeek() == java.time.DayOfWeek.MONDAY)
.collect(Collectors.toList());
System.out.println("Mondays in March: " + mondays);
// [2026-03-02, 2026-03-09, 2026-03-16, 2026-03-23, 2026-03-30]
// Days between two dates
long daysBetween = ChronoUnit.DAYS.between(start, end);
System.out.println("Days between: " + daysBetween); // 6
}
}
Before Java 8, the only date/time classes in the standard library were java.util.Date and java.util.Calendar. You will still encounter them in legacy codebases, older libraries, and frameworks that predate Java 8. Here is what you need to know to work with them and migrate away from them.
Date and Calendar can be modified after creation, leading to bugs when objects are sharedDate.getYear() returns year minus 1900, months are 0-indexed (January = 0)SimpleDateFormat is notorious for causing subtle concurrency bugsDate represents both a date and a time, Calendar mixes concernsimport java.util.Date;
import java.util.Calendar;
import java.text.SimpleDateFormat;
public class LegacyDateProblems {
public static void main(String[] args) throws Exception {
// Problem 1: Mutable objects
Date date = new Date();
Date copy = date; // Not a copy! Same reference.
copy.setTime(0); // This also changes 'date'!
System.out.println("Original date changed: " + date);
// Thu Jan 01 00:00:00 UTC 1970 (modified!)
// Problem 2: Confusing month indexing (0-based)
Calendar cal = Calendar.getInstance();
cal.set(2026, 0, 15); // January is 0, not 1!
System.out.println("Month confusion: " + cal.getTime());
// This is WRONG -- creates February 15, not January!
cal.set(2026, 1, 15); // 1 = February
// Problem 3: getYear() returns year minus 1900
Date now = new Date();
System.out.println("getYear(): " + now.getYear()); // 126, not 2026!
// Problem 4: SimpleDateFormat is not thread-safe
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// DANGER: sharing this across threads causes data corruption
// Use DateTimeFormatter instead (thread-safe)
}
}
When you encounter legacy date code, convert to java.time as early as possible and convert back only at the boundaries (e.g., when calling an old API that requires Date).
import java.time.*;
import java.util.Date;
import java.util.Calendar;
import java.util.GregorianCalendar;
public class LegacyMigration {
public static void main(String[] args) {
// ============================================
// java.util.Date <-> java.time
// ============================================
// Legacy Date -> Instant -> LocalDate / LocalDateTime / ZonedDateTime
Date legacyDate = new Date();
Instant instant = legacyDate.toInstant();
LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.of("America/New_York"));
System.out.println("Legacy Date: " + legacyDate);
System.out.println("As LocalDate: " + localDate);
System.out.println("As LocalDateTime: " + localDateTime);
System.out.println("As ZonedDateTime: " + zonedDateTime);
// Modern -> Legacy Date
Date backToDate = Date.from(
LocalDate.of(2026, 3, 15)
.atStartOfDay(ZoneId.systemDefault())
.toInstant()
);
System.out.println("Back to Date: " + backToDate);
// ============================================
// java.util.Calendar <-> java.time
// ============================================
// Calendar -> ZonedDateTime (GregorianCalendar has a direct method)
Calendar calendar = Calendar.getInstance();
if (calendar instanceof GregorianCalendar) {
ZonedDateTime fromCal = ((GregorianCalendar) calendar).toZonedDateTime();
System.out.println("Calendar -> ZonedDateTime: " + fromCal);
}
// ZonedDateTime -> Calendar
ZonedDateTime zdt = ZonedDateTime.of(2026, 3, 15, 10, 30, 0, 0,
ZoneId.of("America/New_York"));
GregorianCalendar fromZdt = GregorianCalendar.from(zdt);
System.out.println("ZonedDateTime -> Calendar: " + fromZdt.getTime());
}
}
Even with the improved java.time API, there are pitfalls that catch developers at every experience level. Here are the most common ones and how to avoid them.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class MistakeFormatterCase {
public static void main(String[] args) {
LocalDateTime dateTime = LocalDateTime.of(2026, 3, 15, 14, 30);
// WRONG: mm is minutes, not months!
DateTimeFormatter wrong = DateTimeFormatter.ofPattern("yyyy-mm-dd");
System.out.println("Wrong: " + dateTime.format(wrong)); // 2026-30-15 (oops!)
// CORRECT: MM is months
DateTimeFormatter correct = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println("Correct: " + dateTime.format(correct)); // 2026-03-15
}
}
import java.time.LocalDate;
public class MistakeImmutability {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 3, 15);
// WRONG: plusDays() returns a NEW object, it does NOT modify 'date'
date.plusDays(10); // Return value is thrown away!
System.out.println("Still original: " + date); // 2026-03-15
// CORRECT: capture the return value
LocalDate future = date.plusDays(10);
System.out.println("New date: " + future); // 2026-03-25
}
}
import java.time.LocalDate;
public class MistakeEquality {
public static void main(String[] args) {
LocalDate date1 = LocalDate.parse("2026-03-15");
LocalDate date2 = LocalDate.parse("2026-03-15");
// WRONG: == compares object references
System.out.println("== result: " + (date1 == date2)); // May be false!
// CORRECT: use equals() or isEqual()
System.out.println("equals: " + date1.equals(date2)); // true
System.out.println("isEqual: " + date1.isEqual(date2)); // true
}
}
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
public class MistakeParseException {
// BAD: lets exception propagate unhandled
public static LocalDate parseDateUnsafe(String input) {
return LocalDate.parse(input); // Throws DateTimeParseException if invalid
}
// GOOD: handles the exception gracefully
public static Optional parseDateSafe(String input, DateTimeFormatter formatter) {
try {
return Optional.of(LocalDate.parse(input, formatter));
} catch (DateTimeParseException e) {
System.out.println("Invalid date: '" + input + "' - " + e.getMessage());
return Optional.empty();
}
}
public static void main(String[] args) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
parseDateSafe("2026-03-15", fmt).ifPresent(d ->
System.out.println("Parsed: " + d)); // Parsed: 2026-03-15
parseDateSafe("not-a-date", fmt); // Invalid date: 'not-a-date' - ...
parseDateSafe("2026-13-01", fmt); // Invalid date: '2026-13-01' - ... (no month 13)
parseDateSafe("2026-02-30", fmt); // Invalid date: '2026-02-30' - ... (Feb has no 30th)
}
}
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class MistakeWrongType {
public static void main(String[] args) {
// BAD: Using LocalDateTime for a meeting across time zones
LocalDateTime meeting = LocalDateTime.of(2026, 6, 15, 10, 0);
// Is this 10 AM in New York? London? Tokyo? Nobody knows!
// GOOD: Using ZonedDateTime makes the time zone explicit
ZonedDateTime properMeeting = ZonedDateTime.of(2026, 6, 15, 10, 0, 0, 0,
ZoneId.of("America/New_York"));
// Unambiguous: 10 AM Eastern time
System.out.println("Ambiguous: " + meeting);
System.out.println("Clear: " + properMeeting);
}
}
import java.time.LocalDate;
import java.util.Date;
public class MistakeMutableField {
// BAD: legacy Date is mutable -- callers can modify your internal state
static class BadEvent {
private Date eventDate;
public BadEvent(Date date) { this.eventDate = date; }
public Date getEventDate() { return eventDate; } // Exposes mutable reference!
}
// GOOD: use LocalDate (immutable) -- no defensive copying needed
static class GoodEvent {
private final LocalDate eventDate;
public GoodEvent(LocalDate date) { this.eventDate = date; }
public LocalDate getEventDate() { return eventDate; } // Safe! Immutable.
}
public static void main(String[] args) {
// Demonstrating the problem with mutable Date
Date mutableDate = new Date();
BadEvent bad = new BadEvent(mutableDate);
System.out.println("Before: " + bad.getEventDate());
bad.getEventDate().setTime(0); // Caller modifies internal state!
System.out.println("After: " + bad.getEventDate());
// Thu Jan 01 00:00:00 UTC 1970 -- internal state corrupted!
// No such problem with LocalDate
GoodEvent good = new GoodEvent(LocalDate.of(2026, 3, 15));
// good.getEventDate() returns an immutable object -- nothing to corrupt
}
}
These guidelines will help you write correct, maintainable date/time code across any Java project.
| # | Practice | Reason |
|---|---|---|
| 1 | Use java.time for all new code |
Immutable, thread-safe, and well-designed. Never use java.util.Date or Calendar in new code. |
| 2 | Store timestamps as UTC in databases | Avoids ambiguity. Convert to local time zones only at the presentation layer. |
| 3 | Use ISO 8601 format for APIs | yyyy-MM-ddTHH:mm:ssZ is universally understood and sortable. It is the default format for all java.time toString() methods. |
| 4 | Use the most specific type | LocalDate for dates-only, LocalTime for times-only, ZonedDateTime for user-facing date-times, Instant for machine timestamps. |
| 5 | Use IANA zone IDs, not abbreviations | "America/New_York" not "EST". Abbreviations are ambiguous and do not handle DST correctly. |
| 6 | Always handle DateTimeParseException |
User input and external data can always be malformed. Never let parse exceptions crash your application. |
| 7 | Reuse DateTimeFormatter instances |
Formatters are immutable and thread-safe. Create them as static final constants and share them. |
| 8 | Use Clock for testable code |
Instead of LocalDate.now(), inject a Clock so you can test with fixed times: LocalDate.now(clock). |
| 9 | Prefer isBefore()/isAfter() over compareTo() |
More readable and less error-prone than checking if compareTo() returns negative/positive. |
| 10 | Never use == to compare date objects |
Always use equals() or isEqual(). The == operator compares references, not values. |
Hardcoding LocalDate.now() makes your code impossible to test deterministically. Inject a Clock instead:
import java.time.*;
public class TestableService {
private final Clock clock;
// Production constructor uses system clock
public TestableService() {
this(Clock.systemDefaultZone());
}
// Test constructor accepts any clock
public TestableService(Clock clock) {
this.clock = clock;
}
public boolean isWeekend() {
DayOfWeek day = LocalDate.now(clock).getDayOfWeek();
return day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY;
}
public boolean isExpired(LocalDate expiryDate) {
return LocalDate.now(clock).isAfter(expiryDate);
}
public static void main(String[] args) {
// Production usage
TestableService prod = new TestableService();
System.out.println("Is weekend (real): " + prod.isWeekend());
// Test: freeze time to a known Wednesday
Clock fixedClock = Clock.fixed(
LocalDate.of(2026, 3, 11).atStartOfDay(ZoneId.systemDefault()).toInstant(),
ZoneId.systemDefault()
);
TestableService test = new TestableService(fixedClock);
System.out.println("Is weekend (Wed): " + test.isWeekend()); // false
// Test: freeze time to a known Saturday
Clock saturdayClock = Clock.fixed(
LocalDate.of(2026, 3, 14).atStartOfDay(ZoneId.systemDefault()).toInstant(),
ZoneId.systemDefault()
);
TestableService satTest = new TestableService(saturdayClock);
System.out.println("Is weekend (Sat): " + satTest.isWeekend()); // true
}
}
Let us bring everything together with a realistic event scheduling system that demonstrates most of the concepts covered in this tutorial. This system creates events with dates and times, handles time zones, checks for conflicts, formats output, and calculates durations.
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;
public class EventSchedulingSystem {
// ============================================
// Event class
// ============================================
static class Event {
private final String name;
private final ZonedDateTime start;
private final Duration duration;
private final String organizer;
public Event(String name, ZonedDateTime start, Duration duration, String organizer) {
if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
if (start.isBefore(ZonedDateTime.now())) {
// Allow past events for demo purposes, but warn
System.out.println(" [WARN] Event '" + name + "' is in the past.");
}
this.name = name;
this.start = start;
this.duration = duration;
this.organizer = organizer;
}
public String getName() { return name; }
public ZonedDateTime getStart() { return start; }
public ZonedDateTime getEnd() { return start.plus(duration); }
public Duration getDuration() { return duration; }
public String getOrganizer() { return organizer; }
public boolean conflictsWith(Event other) {
return this.getStart().isBefore(other.getEnd()) &&
this.getEnd().isAfter(other.getStart());
}
public String formatForZone(ZoneId zone) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("EEE, MMM dd yyyy 'at' hh:mm a z");
ZonedDateTime localStart = start.withZoneSameInstant(zone);
ZonedDateTime localEnd = getEnd().withZoneSameInstant(zone);
DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("hh:mm a z");
return String.format("%s: %s - %s",
name, localStart.format(fmt), localEnd.format(timeFmt));
}
@Override
public String toString() {
return formatForZone(start.getZone());
}
}
// ============================================
// EventScheduler class
// ============================================
static class EventScheduler {
private final List events = new ArrayList<>();
private static final DateTimeFormatter DISPLAY_FORMAT =
DateTimeFormatter.ofPattern("MMM dd, yyyy HH:mm z");
public boolean addEvent(Event event) {
for (Event existing : events) {
if (existing.conflictsWith(event)) {
System.out.println(" CONFLICT: '" + event.getName() +
"' overlaps with '" + existing.getName() + "'");
return false;
}
}
events.add(event);
System.out.println(" ADDED: " + event.getName());
return true;
}
public List getEventsOnDate(LocalDate date, ZoneId zone) {
return events.stream()
.filter(e -> {
LocalDate eventDate = e.getStart()
.withZoneSameInstant(zone).toLocalDate();
return eventDate.equals(date);
})
.sorted(Comparator.comparing(Event::getStart))
.collect(Collectors.toList());
}
public List getUpcomingEvents(ZonedDateTime from, int days) {
ZonedDateTime until = from.plusDays(days);
return events.stream()
.filter(e -> !e.getStart().isBefore(from) && e.getStart().isBefore(until))
.sorted(Comparator.comparing(Event::getStart))
.collect(Collectors.toList());
}
public Duration getTotalScheduledTime() {
return events.stream()
.map(Event::getDuration)
.reduce(Duration.ZERO, Duration::plus);
}
public Map> groupByDate(ZoneId zone) {
return events.stream()
.collect(Collectors.groupingBy(
e -> e.getStart().withZoneSameInstant(zone).toLocalDate(),
TreeMap::new,
Collectors.toList()
));
}
}
// ============================================
// Main -- demonstration
// ============================================
public static void main(String[] args) {
EventScheduler scheduler = new EventScheduler();
ZoneId nyZone = ZoneId.of("America/New_York");
ZoneId londonZone = ZoneId.of("Europe/London");
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
System.out.println("=== CREATING EVENTS ===");
System.out.println();
// Event 1: Team standup (New York time)
Event standup = new Event("Team Standup",
ZonedDateTime.of(2026, 6, 15, 9, 0, 0, 0, nyZone),
Duration.ofMinutes(30), "Alice");
scheduler.addEvent(standup);
// Event 2: Architecture review
Event archReview = new Event("Architecture Review",
ZonedDateTime.of(2026, 6, 15, 10, 0, 0, 0, nyZone),
Duration.ofHours(2), "Bob");
scheduler.addEvent(archReview);
// Event 3: Try to book overlapping meeting
Event overlap = new Event("Quick Sync",
ZonedDateTime.of(2026, 6, 15, 11, 0, 0, 0, nyZone),
Duration.ofHours(1), "Charlie");
scheduler.addEvent(overlap); // CONFLICT with Architecture Review
// Event 4: Afternoon meeting
Event deployment = new Event("Deployment Planning",
ZonedDateTime.of(2026, 6, 15, 14, 0, 0, 0, nyZone),
Duration.ofMinutes(45), "Alice");
scheduler.addEvent(deployment);
// Event 5: Next day event in London time
Event londonMeeting = new Event("London Client Call",
ZonedDateTime.of(2026, 6, 16, 10, 0, 0, 0, londonZone),
Duration.ofHours(1), "Diana");
scheduler.addEvent(londonMeeting);
// ============================================
// Display events for different time zones
// ============================================
System.out.println("\n=== MONDAY JUNE 15 SCHEDULE (New York) ===");
LocalDate monday = LocalDate.of(2026, 6, 15);
for (Event e : scheduler.getEventsOnDate(monday, nyZone)) {
System.out.println(" " + e.formatForZone(nyZone));
}
System.out.println("\n=== SAME EVENTS VIEWED FROM TOKYO ===");
for (Event e : scheduler.getEventsOnDate(monday, nyZone)) {
System.out.println(" " + e.formatForZone(tokyoZone));
}
// ============================================
// Stats
// ============================================
System.out.println("\n=== SCHEDULE STATS ===");
Duration total = scheduler.getTotalScheduledTime();
System.out.println("Total scheduled: " + total.toHours() + "h " +
total.toMinutesPart() + "m");
// Days until an event
ZonedDateTime now = ZonedDateTime.of(2026, 2, 28, 12, 0, 0, 0, nyZone);
long daysUntilStandup = ChronoUnit.DAYS.between(now, standup.getStart());
System.out.println("Days until standup: " + daysUntilStandup);
// ============================================
// Period calculation
// ============================================
System.out.println("\n=== TIME UNTIL EVENTS ===");
Period period = Period.between(now.toLocalDate(), standup.getStart().toLocalDate());
System.out.println("Until standup: " + period.getMonths() + " months, " +
period.getDays() + " days");
// ============================================
// Grouped by date
// ============================================
System.out.println("\n=== EVENTS GROUPED BY DATE ===");
Map> grouped = scheduler.groupByDate(nyZone);
DateTimeFormatter dateFmt = DateTimeFormatter.ofPattern("EEEE, MMMM dd");
for (Map.Entry> entry : grouped.entrySet()) {
System.out.println(entry.getKey().format(dateFmt) + ":");
for (Event e : entry.getValue()) {
System.out.println(" - " + e.getName() + " (" +
e.getDuration().toMinutes() + " min, by " + e.getOrganizer() + ")");
}
}
// Output:
// === CREATING EVENTS ===
//
// ADDED: Team Standup
// ADDED: Architecture Review
// CONFLICT: 'Quick Sync' overlaps with 'Architecture Review'
// ADDED: Deployment Planning
// ADDED: London Client Call
//
// === MONDAY JUNE 15 SCHEDULE (New York) ===
// Team Standup: Mon, Jun 15 2026 at 09:00 AM EDT - 09:30 AM EDT
// Architecture Review: Mon, Jun 15 2026 at 10:00 AM EDT - 12:00 PM EDT
// Deployment Planning: Mon, Jun 15 2026 at 02:00 PM EDT - 02:45 PM EDT
//
// === SAME EVENTS VIEWED FROM TOKYO ===
// Team Standup: Mon, Jun 15 2026 at 10:00 PM JST - 10:30 PM JST
// Architecture Review: Mon, Jun 15 2026 at 11:00 PM JST - 01:00 AM JST
// Deployment Planning: Tue, Jun 16 2026 at 03:00 AM JST - 03:45 AM JST
//
// === SCHEDULE STATS ===
// Total scheduled: 4h 15m
// Days until standup: 107
//
// === TIME UNTIL EVENTS ===
// Until standup: 3 months, 15 days
//
// === EVENTS GROUPED BY DATE ===
// Monday, June 15:
// - Team Standup (30 min, by Alice)
// - Architecture Review (120 min, by Bob)
// - Deployment Planning (45 min, by Alice)
// Tuesday, June 16:
// - London Client Call (60 min, by Diana)
}
}
| # | Concept | Where It Appears |
|---|---|---|
| 1 | ZonedDateTime creation |
All events are created with explicit time zones |
| 2 | Time zone conversion | formatForZone() uses withZoneSameInstant() |
| 3 | Duration |
Event duration, total scheduled time |
| 4 | Period |
Human-readable time until events |
| 5 | ChronoUnit |
Days between now and a future event |
| 6 | DateTimeFormatter |
Custom display formats for dates and times |
| 7 | Conflict detection | conflictsWith() using isBefore()/isAfter() |
| 8 | LocalDate extraction |
Grouping events by date |
| 9 | Immutability | All date/time objects are safely shared |
| 10 | Stream operations on dates | Filtering, sorting, and grouping events |
| Task | Code |
|---|---|
| Current date | LocalDate.now() |
| Current time | LocalTime.now() |
| Current date-time | LocalDateTime.now() |
| Current instant (UTC) | Instant.now() |
| Specific date | LocalDate.of(2026, 3, 15) |
| Parse date string | LocalDate.parse("2026-03-15") |
| Format a date | date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")) |
| Add days | date.plusDays(10) |
| Subtract months | date.minusMonths(3) |
| Compare dates | date1.isBefore(date2) |
| Days between dates | ChronoUnit.DAYS.between(date1, date2) |
| Period between dates | Period.between(date1, date2) |
| Duration between times | Duration.between(time1, time2) |
| Convert zone | zdt.withZoneSameInstant(ZoneId.of("Asia/Tokyo")) |
| First day of month | date.with(TemporalAdjusters.firstDayOfMonth()) |
| Last day of month | date.with(TemporalAdjusters.lastDayOfMonth()) |
| Next Monday | date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)) |
| Is leap year | date.isLeapYear() |
| Legacy Date to Instant | legacyDate.toInstant() |
| Instant to legacy Date | Date.from(instant) |