Date Basics
Dates and times appear in almost every application: order timestamps, due dates, subscription renewals, meeting schedules, log entries, ages, and deadlines. Java's modern date and time API, the java.time package introduced in Java 8, provides clear, immutable, thread-safe classes for each kind of temporal value. This lesson explains how the API is organized, which class to use for which job, and the basic operations they all share. The following lessons cover the individual classes in detail.
The Old API and Why It Was Replaced
Before Java 8, programs used java.util.Date, java.util.Calendar, and java.text.SimpleDateFormat. They are still found in older code, but they have well-known problems:
| Problem | Example |
|---|---|
| Mutable objects | A Date passed to a method could be changed by that method |
| Confusing design | Date represents an instant, yet its toString() shows a local date and time |
| Zero-based months | In Calendar, January is month 0 |
| Not thread-safe | A shared SimpleDateFormat produces corrupted results under concurrent use |
| No separate types | No way to represent "a date without a time" or "a time without a date" |
java.time fixes all of these. New code should use it exclusively, converting to legacy types only when an older library requires them.
The Core Classes
Each class represents a different concept. Choosing the right one is the most important decision:
| Class | Represents | Example value | Typical use |
|---|---|---|---|
LocalDate | A date without time or zone | 2026-11-12 | Birthdays, due dates, holidays |
LocalTime | A time of day without date or zone | 10:00 | Opening hours, alarm times |
LocalDateTime | Date and time without zone | 2026-11-12T10:00 | A form's "appointment" field before a zone is known |
ZonedDateTime | Date and time in a specific time zone | 2026-11-12T10:00+05:30[Asia/Kolkata] | Meetings, flights, anything scheduled for a place |
OffsetDateTime | Date and time with a fixed UTC offset | 2026-11-12T10:00+05:30 | Timestamps in APIs and databases |
Instant | A point on the global timeline (UTC) | 2026-11-12T04:30:00Z | Logging, event timestamps, storing "when it happened" |
Duration | An amount of time in seconds and nanoseconds | PT8H30M | Elapsed time, timeouts |
Period | An amount of time in years, months, and days | P1Y2M3D | Ages, subscription lengths |
ZoneId | A time zone with its rules | Asia/Kolkata | Converting between local times |
"Local" means no time zone: a LocalDate of 2026-11-12 is "the 12th of November" wherever you are, not a specific moment. An Instant is the opposite: one exact moment, the same everywhere in the world, but meaningless to a person until it is shown in some time zone.
Getting the Current Date and Time
LocalDate today = LocalDate.now();
LocalTime time = LocalTime.now();
LocalDateTime dt = LocalDateTime.now();
ZonedDateTime zdt = ZonedDateTime.now(); // in the JVM's default zone
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
Instant now = Instant.now();
With a fixed moment (04:30 UTC on 12 November 2026) and the Asia/Kolkata zone, these produce:
2026-11-12 | 10:00 | 2026-11-12T10:00
2026-11-12T10:00+05:30[Asia/Kolkata] | 2026-11-12T04:30:00Z
All java.time types print in ISO-8601 format by default (yyyy-MM-dd, HH:mm:ss, T between date and time, Z for UTC), which is also the standard format for JSON and APIs.
Testable time with Clock
Code that calls now() directly is hard to test, because the result changes every run. Every now() method also accepts a java.time.Clock. Production code passes Clock.systemDefaultZone(), and tests pass a fixed clock:
Clock fixed = Clock.fixed(Instant.parse("2026-11-12T04:30:00Z"), ZoneId.of("Asia/Kolkata"));
LocalDate.now(fixed); // always 2026-11-12
Creating Specific Dates and Times
LocalDate d = LocalDate.of(2026, 11, 12); // months are 1-12
LocalDate d2 = LocalDate.of(2026, Month.NOVEMBER, 12);
LocalTime t = LocalTime.of(9, 30);
LocalDateTime dt = LocalDateTime.of(d, t);
LocalDate parsed = LocalDate.parse("2026-11-12"); // ISO text
Instant i = Instant.parse("2026-11-12T04:30:00Z");
Invalid values are rejected immediately:
LocalDate.of(2026, 2, 30); // DateTimeException: Invalid date 'FEBRUARY 30'
Immutability
Every java.time object is immutable. Methods that "change" a value return a new object:
LocalDate d = LocalDate.of(2026, 1, 31);
d.plusDays(1); // result discarded
System.out.println(d); // 2026-01-31: unchanged
LocalDate next = d.plusDays(1); // 2026-02-01
Immutability makes these objects safe to share between threads and to use as map keys. As with String, always keep the result.
Common Operations
All the main classes share a consistent method vocabulary:
| Kind | Methods | Example |
|---|---|---|
| Create | of, parse, now, from | LocalDate.of(2026, 11, 12) |
| Read a field | getYear, getMonth, getDayOfWeek, getHour | d.getDayOfWeek() → SATURDAY |
| Add or subtract | plusDays, minusMonths, plus(Duration) | d.plusMonths(1) |
| Replace a field | withYear, withDayOfMonth, with(adjuster) | d.withDayOfMonth(1) |
| Compare | isBefore, isAfter, isEqual, compareTo | due.isBefore(today) |
| Convert | atTime, atZone, toLocalDate, toInstant | d.atStartOfDay(zone) |
| Format | format(DateTimeFormatter) | d.format(...) |
Date arithmetic follows calendar rules:
LocalDate.of(2026, 1, 31).plusMonths(1); // 2026-02-28: clamped to the last valid day
LocalDate.of(2028, 1, 1).isLeapYear(); // true
Measuring Time Between Values
LocalDate birthday = LocalDate.of(1998, Month.FEBRUARY, 14);
LocalDate today = LocalDate.of(2026, 11, 12);
Period.between(birthday, today).getYears(); // 28 (age)
ChronoUnit.DAYS.between(today, LocalDate.of(2027, 2, 14)); // 94
Duration.between(LocalTime.of(9, 15), LocalTime.of(17, 45)); // PT8H30M
Period measures calendar-based amounts (years, months, days); Duration measures exact time (hours, minutes, seconds). ChronoUnit.X.between returns a single count in one unit.
Converting to and from Legacy Types
Date legacy = Date.from(Instant.now()); // java.time → java.util.Date
Instant instant = legacy.toInstant(); // java.util.Date → java.time
LocalDate fromSql = sqlDate.toLocalDate(); // java.sql.Date → LocalDate
Modern JDBC drivers also read and write LocalDate, LocalDateTime, and OffsetDateTime directly, so conversions are needed mainly at the boundary with older libraries.
Choosing the Right Type
- Something that happened at an exact moment (a payment, a login, a log entry): store an
Instant(orOffsetDateTimein UTC). - Something that happens at a local time in a place (a class at 7:30 pm in Chennai):
ZonedDateTime, or aLocalDateTimeplus aZoneIdstored separately. - A calendar date with no time (a birthday, an invoice due date):
LocalDate. - A recurring time of day (shop opens at 9:00):
LocalTime. - How long something takes:
Duration. How many calendar units:Period.
A frequent bug is storing a moment as a LocalDateTime: the value no longer says which time zone it belongs to, and conversions later become guesswork.
Practical Example: A Webinar Across Time Zones
A webinar is scheduled at 7:30 pm Indian time. Registrants around the world need to see the start time in their own zones, and the system needs to know how long remains and when to send a reminder.
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.List;
public class Webinar {
public static void main(String[] args) {
Clock clock = Clock.fixed(Instant.parse("2026-11-12T04:30:00Z"), ZoneId.of("Asia/Kolkata"));
ZonedDateTime start = ZonedDateTime.of(LocalDate.of(2026, 11, 20), LocalTime.of(19, 30), ZoneId.of("Asia/Kolkata"));
Duration length = Duration.ofMinutes(90);
Instant startInstant = start.toInstant();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("EEE dd MMM, hh:mm a (zzz)");
for (String zone : List.of("Asia/Kolkata", "Europe/London", "America/New_York", "Asia/Tokyo")) {
ZonedDateTime local = startInstant.atZone(ZoneId.of(zone));
System.out.printf("%-17s %s%n", zone, local.format(fmt));
}
LocalDate today = LocalDate.now(clock);
long daysLeft = ChronoUnit.DAYS.between(today, start.toLocalDate());
Duration untilStart = Duration.between(Instant.now(clock), startInstant);
System.out.println("Today: " + today + " (" + today.getDayOfWeek() + ")");
System.out.println("Days left: " + daysLeft + ", exactly " + untilStart.toDays() + "d " + untilStart.toHoursPart() + "h");
System.out.println("Ends at: " + start.plus(length).toLocalTime());
System.out.println("Reminder date: " + start.toLocalDate().minusDays(1));
}
}
Output (with an English default locale):
Asia/Kolkata Fri 20 Nov, 07:30 PM (IST)
Europe/London Fri 20 Nov, 02:00 PM (GMT)
America/New_York Fri 20 Nov, 09:00 AM (EST)
Asia/Tokyo Fri 20 Nov, 11:00 PM (JST)
Today: 2026-11-12 (THURSDAY)
Days left: 8, exactly 8d 9h
Ends at: 21:00
Reminder date: 2026-11-19
Each class plays its own role:
ZonedDateTimeholds the scheduled start, because the event happens at a local time in a specific zone.Instantconverts that start into one global moment.atZonethen shows that moment in each attendee's time zone, applying every zone's current offset rules: London is five and a half hours behind India in November, and New York ten and a half.LocalDateandChronoUnit.DAYScount calendar days remaining.Durationgives the exact time remaining.plus(length)andminusDayscompute the end time and the reminder date.
Using a fixed Clock makes the program's output the same on every run, which is exactly how such logic is unit-tested.