Java Tutorial
🔍

Java Date and Time API

Java Date and Time API

The java.time package, introduced in Java 8, is the API for representing and working with dates, times, and time-based amounts. It replaced java.util.Date and java.util.Calendar because both of those classes turned out to be genuinely unsafe to use correctly — mutable, not thread-safe, and confusing enough that almost every Java codebase built before 2014 has at least one date-related bug traceable directly back to them. Every class in java.time is immutable, each one has a narrow and specific purpose instead of one class trying to do everything, and the whole package was designed by the same engineer who built the widely-used Joda-Time library specifically to fix what Date and Calendar got wrong.

What Is the Date and Time API?

java.time is a set of immutable, thread-safe classes, each modeling a distinct kind of date or time concept rather than one general-purpose class trying to cover every case. LocalDate represents a date with no time or time zone attached — a birthday or a due date. LocalTime represents a time of day with no date attached. LocalDateTime combines both, still with no time zone. ZonedDateTime adds a time zone on top of that, for the cases where the zone genuinely matters. Instant represents a single point on the machine timeline, typically used for timestamps. Period measures a date-based amount, like "2 months and 5 days." Duration measures a time-based amount, like "3 hours." DateTimeFormatter converts between all of these objects and their String representations.

One sentence before the diagram: each class in the family carries exactly the pieces its name promises, and nothing more.

LocalDate            LocalTime            LocalDateTime
(date only,           (time only,           (date + time,
 no time,              no date,              still no zone)
 no zone)              no zone)                    |
      \                     \                       |
       \_____________________\______  +ZoneId  ____/
                                    \            /
                                 ZonedDateTime
                        (date + time + zone = one
                         unambiguous real-world moment)

Instant  --  a single point on the UTC timeline, with no
             date, time, or zone fields of its own at all

A LocalDate, LocalTime, or LocalDateTime can be built and compared freely without ever touching a time zone — the zone only enters the picture once ZonedDateTime or Instant is actually needed, which is exactly why choosing the narrowest class that fits the data being modeled matters as much as it does.

Why the Date and Time API Was Introduced

Before Java 8, even a simple task like adding ten days to a date meant working through Calendar's awkward, mutable API, including its infamous zero-indexed months.

1// File: BeforeDateTimeApi.java 2import java.util.*; 3import java.text.*; 4 5public class BeforeDateTimeApi { 6 public static void main(String[] args) { 7 Calendar calendar = Calendar.getInstance(); 8 calendar.set(2026, Calendar.JANUARY, 15); // month is zero-indexed - JANUARY is 0 9 calendar.add(Calendar.DAY_OF_MONTH, 10); 10 11 Date result = calendar.getTime(); 12 SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); 13 System.out.println(formatter.format(result)); 14 } 15}
Output:
2026-01-25

The same calculation with java.time reads directly as what it means, and printing a LocalDate gives a clean, unambiguous format without needing a separate formatter at all.

1// File: AfterDateTimeApi.java 2import java.time.*; 3 4public class AfterDateTimeApi { 5 public static void main(String[] args) { 6 LocalDate startDate = LocalDate.of(2026, Month.JANUARY, 15); // January named directly, no zero-indexing 7 LocalDate result = startDate.plusDays(10); 8 9 System.out.println(result); 10 } 11}
Output:
2026-01-25

Both versions land on the same date. The java.time version needed no formatter to produce readable output, no mutable Calendar object being modified step by step, and no month-numbering trap waiting to catch the next person who edits this code.

Syntax

A quick tour of the core types shows how each one models a distinct piece of the date-and-time picture.

1// File: DateTimeApiOverviewDemo.java 2import java.time.*; 3import java.time.format.*; 4import java.util.Locale; 5 6public class DateTimeApiOverviewDemo { 7 public static void main(String[] args) { 8 LocalDate date = LocalDate.of(2026, 3, 10); 9 LocalTime time = LocalTime.of(14, 30); 10 LocalDateTime dateTime = LocalDateTime.of(date, time); 11 ZonedDateTime zonedDateTime = dateTime.atZone(ZoneId.of("Asia/Kolkata")); 12 13 Period gap = Period.between(LocalDate.of(2026, 1, 1), date); 14 Duration meetingLength = Duration.ofMinutes(90); 15 16 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy", Locale.ENGLISH); 17 18 System.out.println("LocalDate: " + date); 19 System.out.println("LocalTime: " + time); 20 System.out.println("LocalDateTime: " + dateTime); 21 System.out.println("ZonedDateTime: " + zonedDateTime); 22 System.out.println("Period since Jan 1: " + gap); 23 System.out.println("Meeting length: " + meetingLength); 24 System.out.println("Formatted date: " + date.format(formatter)); 25 } 26}
Output:
LocalDate: 2026-03-10
LocalTime: 14:30
LocalDateTime: 2026-03-10T14:30
ZonedDateTime: 2026-03-10T14:30+05:30[Asia/Kolkata]
Period since Jan 1: P2M9D
Meeting length: PT1H30M
Formatted date: 10-Mar-2026

Period and Duration both print in ISO-8601 format by default — P2M9D reads as "2 months, 9 days" and PT1H30M reads as "1 hour, 30 minutes," with the T marking where the time portion begins. Recognizing this format on sight saves real confusion the first time it shows up in a log or a debugger.

Common Use Cases

Calculating an Age or a Gap Between Dates

Period.between() breaks the gap between two dates down into years, months, and days, exactly the shape a human-readable age or tenure calculation needs.

1// File: AgeCalculationExample.java 2import java.time.*; 3 4public class AgeCalculationExample { 5 public static void main(String[] args) { 6 LocalDate birthDate = LocalDate.of(1998, 7, 22); 7 LocalDate referenceDate = LocalDate.of(2026, 8, 25); 8 9 Period age = Period.between(birthDate, referenceDate); 10 11 System.out.println("Age: " + age.getYears() + " years, " + age.getMonths() + " months, " + age.getDays() + " days"); 12 } 13}
Output:
Age: 28 years, 1 months, 3 days

Parsing a Date From User Input

DateTimeFormatter reads a raw String into a proper LocalDate when the incoming format is known in advance, which is exactly what most form inputs and API payloads look like.

1// File: ParsingDateExample.java 2import java.time.*; 3import java.time.format.*; 4 5public class ParsingDateExample { 6 public static void main(String[] args) { 7 String rawInput = "25/08/2026"; 8 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 9 10 LocalDate parsedDate = LocalDate.parse(rawInput, formatter); 11 12 System.out.println("Parsed date: " + parsedDate); 13 System.out.println("Day of week: " + parsedDate.getDayOfWeek()); 14 } 15}
Output:
Parsed date: 2026-08-25
Day of week: TUESDAY

Comparing Two Dates

isBefore() and isAfter() express date comparisons directly, without the manual integer-comparison dance Calendar.compareTo() used to require.

1// File: DateComparisonExample.java 2import java.time.*; 3 4public class DateComparisonExample { 5 public static void main(String[] args) { 6 LocalDate subscriptionExpiry = LocalDate.of(2026, 6, 30); 7 LocalDate today = LocalDate.of(2026, 8, 25); 8 9 boolean isExpired = today.isAfter(subscriptionExpiry); 10 11 System.out.println("Subscription expired: " + isExpired); 12 } 13}
Output:
Subscription expired: true

Measuring Elapsed Time

Duration.between() is the natural fit for measuring how long something took, which shows up constantly in logging and performance monitoring.

1// File: DurationBetweenExample.java 2import java.time.*; 3 4public class DurationBetweenExample { 5 public static void main(String[] args) { 6 LocalDateTime requestReceived = LocalDateTime.of(2026, 8, 25, 10, 15, 0); 7 LocalDateTime responseSent = LocalDateTime.of(2026, 8, 25, 10, 15, 42); 8 9 Duration processingTime = Duration.between(requestReceived, responseSent); 10 11 System.out.println("Processing time: " + processingTime.getSeconds() + " seconds"); 12 } 13}
Output:
Processing time: 42 seconds

Real-World Example

A subscription service running a free trial needs to calculate exactly when that trial ends, whether it is still active as of a given day, and how many days remain — the kind of logic that decides whether a customer keeps access or gets prompted to pay. Getting the boundary date right matters here: a trial that "ends on" a date should still count that date as active, not cut the customer off a day early.

1// File: Subscription.java 2import java.time.*; 3 4public record Subscription(String planName, LocalDate startDate, int trialDays) {}
1// File: SubscriptionService.java 2import java.time.*; 3import java.time.format.*; 4import java.time.temporal.ChronoUnit; 5import java.util.Locale; 6 7public class SubscriptionService { 8 private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.ENGLISH); 9 10 public LocalDate trialEndDate(Subscription subscription) { 11 return subscription.startDate().plusDays(subscription.trialDays()); 12 } 13 14 public boolean isTrialActive(Subscription subscription, LocalDate asOfDate) { 15 LocalDate trialEnd = trialEndDate(subscription); 16 return !asOfDate.isAfter(trialEnd); 17 } 18 19 public long daysRemainingInTrial(Subscription subscription, LocalDate asOfDate) { 20 LocalDate trialEnd = trialEndDate(subscription); 21 if (asOfDate.isAfter(trialEnd)) { 22 return 0; 23 } 24 return ChronoUnit.DAYS.between(asOfDate, trialEnd); 25 } 26 27 public String formatForDisplay(LocalDate date) { 28 return date.format(DISPLAY_FORMAT); 29 } 30}
1// File: SubscriptionDemo.java 2import java.time.*; 3 4public class SubscriptionDemo { 5 public static void main(String[] args) { 6 Subscription plan = new Subscription("Swadeshi Plus", LocalDate.of(2026, 8, 10), 15); 7 8 SubscriptionService service = new SubscriptionService(); 9 10 LocalDate trialEnd = service.trialEndDate(plan); 11 LocalDate today = LocalDate.of(2026, 8, 20); 12 13 System.out.println("Trial ends on: " + service.formatForDisplay(trialEnd)); 14 System.out.println("Trial active today: " + service.isTrialActive(plan, today)); 15 System.out.println("Days remaining: " + service.daysRemainingInTrial(plan, today)); 16 17 LocalDate laterCheck = LocalDate.of(2026, 8, 27); 18 System.out.println("Trial active on 27 Aug: " + service.isTrialActive(plan, laterCheck)); 19 } 20}
Output:
Trial ends on: 25 Aug 2026
Trial active today: true
Days remaining: 5
Trial active on 27 Aug: false

A mistake that appears often in fresher pull requests is checking asOfDate.isEqual(trialEnd) on its own instead of isAfter(trialEnd), which quietly cuts a customer off a day early on every date before the boundary. Using isAfter() and treating the trial's own end date as still active, exactly as isTrialActive does here, is what actually matches how most products communicate a trial-end date to customers.

Combining the Date and Time API With Other Features

Every class in java.time is an immutable value object, the same design principle behind immutable class design and record classes covered elsewhere in this series — LocalDate, in fact, behaves a great deal like a record built specifically around calendar fields. DateTimeFormatter and LocalDate.parse() throw DateTimeParseException, a runtime exception that fits naturally into the same exception-handling patterns used everywhere else in Java. Stream pipelines commonly filter and map over collections of dates — finding every subscription expiring this week, for instance — which is exactly where the Streams API and this package meet in real code.

Best Practices

Avoid java.util.Date and java.util.Calendar in new code entirely. Everything they can genuinely do is available in java.time, with a design that never requires defensive copying just to stay safe.

Choose the narrowest class that matches what is actually being modeled. LocalDate for a birthday or a due date with no time component, LocalDateTime for a timestamp where time zone genuinely does not matter, ZonedDateTime only when the zone itself is part of the problem, such as scheduling something across regions.

Reuse a single DateTimeFormatter instance rather than constructing a new one for every call. Like the rest of java.time, DateTimeFormatter is immutable and thread-safe, and rebuilding it repeatedly gains nothing.

Store timestamps as Instant, or explicitly in UTC, when persisting them to a database, and convert to a specific ZonedDateTime only at the point data is actually displayed to a user.

Common Mistakes

Assuming a java.time object can be mutated in place is one of the fastest ways to introduce a silent bug, since every method that looks like it changes a date actually returns a brand new one.

1// File: ImmutabilityMistake.java 2import java.time.*; 3 4public class ImmutabilityMistake { 5 public static void main(String[] args) { 6 LocalDate originalDate = LocalDate.of(2026, 1, 1); 7 8 originalDate.plusDays(10); // return value is discarded - originalDate itself never changes 9 10 System.out.println("originalDate after plusDays(10): " + originalDate); 11 12 LocalDate updatedDate = originalDate.plusDays(10); // the return value must be captured 13 System.out.println("updatedDate: " + updatedDate); 14 } 15}
Output:
originalDate after plusDays(10): 2026-01-01
updatedDate: 2026-01-11

Comparing two java.time objects with == checks object identity, not the calendar value they represent, and two separately constructed dates for the same day are almost never the same object in memory.

1// File: EqualityMistake.java 2import java.time.*; 3 4public class EqualityMistake { 5 public static void main(String[] args) { 6 LocalDate first = LocalDate.of(2026, 5, 1); 7 LocalDate second = LocalDate.of(2026, 5, 1); 8 9 System.out.println("first == second: " + (first == second)); 10 System.out.println("first.equals(second): " + first.equals(second)); 11 System.out.println("first.isEqual(second): " + first.isEqual(second)); 12 } 13}
Output:
first == second: false
first.equals(second): true
first.isEqual(second): true

Parsing a date string with a pattern that does not match its actual format throws DateTimeParseException at runtime rather than failing at compile time, so the mismatch only surfaces once real input reaches it.

1// File: ParsePatternMismatchMistake.java 2import java.time.*; 3import java.time.format.*; 4 5public class ParsePatternMismatchMistake { 6 public static void main(String[] args) { 7 String rawInput = "2026-08-25"; 8 9 try { 10 DateTimeFormatter wrongFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy"); 11 LocalDate.parse(rawInput, wrongFormatter); 12 } catch (DateTimeParseException e) { 13 System.out.println("DateTimeParseException - the pattern does not match the input format"); 14 } 15 16 DateTimeFormatter correctFormatter = DateTimeFormatter.ISO_LOCAL_DATE; 17 LocalDate parsed = LocalDate.parse(rawInput, correctFormatter); 18 System.out.println("Parsed correctly: " + parsed); 19 } 20}
Output:
DateTimeParseException - the pattern does not match the input format
Parsed correctly: 2026-08-25

Interview Questions

Q1. Why was the java.time API introduced when java.util.Date and Calendar already existed?

Date and Calendar were mutable, not thread-safe, and carried design flaws like zero-indexed months and a Date class that conflated a timestamp with a display format. java.time, introduced in Java 8, replaced both with a set of immutable, thread-safe classes, each modeling one specific concept — a date, a time, a duration — instead of one class trying to represent every possible use case. Interviewers ask this to see whether a candidate understands the actual design failures being fixed, not just that "a new API showed up in Java 8."

Q2. What is the difference between LocalDate, LocalDateTime, and ZonedDateTime?

LocalDate represents a date with no time or time zone at all — a birthday or a deadline. LocalDateTime adds a time of day but still carries no time zone information, making it unsuitable for anything that needs to be compared across regions. ZonedDateTime adds an explicit time zone on top of a date and time, and it is the only one of the three that can be safely converted to a different zone or used to reason about "the same instant" as seen from two different places.

Q3. Why are java.time classes immutable, and what problem does that solve compared to Date?

Immutability means every method that appears to modify a date, like plusDays(), actually returns a new object and leaves the original untouched. This eliminates an entire category of bug that plagued Date and Calendar — a shared, mutable date object being changed unexpectedly by one part of a codebase while another part still held a reference to it, assuming it had not changed. It also makes java.time objects inherently safe to share across threads without synchronization, since nothing about them can ever change after construction.

Q4. What is the difference between Period and Duration?

Period measures a date-based amount of time — years, months, and days — and is meant for calendar concepts like "2 months and 5 days" where the actual elapsed time varies depending on which months are involved. Duration measures a time-based amount using seconds and nanoseconds, suited for precise, fixed-length spans like "90 minutes." Using Period when a fixed, unambiguous span of time is actually needed, or vice versa, is a subtle mistake that shows up in code review once someone tries to add a Period to an Instant, which does not compile.

Q5. Is DateTimeFormatter thread-safe, and how does that compare to SimpleDateFormat?

Yes, DateTimeFormatter is immutable and thread-safe, so a single instance can be shared freely across threads and reused indefinitely. SimpleDateFormat, its legacy counterpart, is explicitly not thread-safe — sharing one instance across threads without external synchronization is a well-known source of corrupted output and hard-to-reproduce bugs in older codebases, which is exactly the kind of defensive-copying overhead java.time was designed to eliminate.

Q6. How would you store and compare timestamps correctly across multiple time zones?

Store the timestamp as an Instant, or explicitly in UTC, since Instant represents a single, unambiguous point on the timeline with no time zone attached to argue about. Convert to a specific ZonedDateTime only at the point the timestamp needs to be displayed to a user in their local time zone. Comparing LocalDateTime values across regions without a time zone attached is a common production bug, since two LocalDateTime values that look identical could represent two entirely different actual moments depending on where each one originated.

FAQs

Can I still use java.util.Date in new Java code?

Technically yes, since it has not been removed from the JDK, but it should not be reached for in new code. Every legitimate use case it covers is handled better by java.time, and many existing APIs, including JDBC and some legacy libraries, still return Date objects that need converting to java.time types anyway.

What is Instant, and how is it different from LocalDateTime?

Instant represents a single point on the UTC timeline, typically used for machine timestamps like "when this event occurred." LocalDateTime represents a date and time with no time zone context at all, meant for human-facing concepts like "this meeting starts at 3 PM" where the zone is understood from context rather than stored explicitly.

Does LocalDate.now() depend on the system's time zone?

Yes. LocalDate.now() without arguments uses the JVM's default time zone to determine what "today" means, which can produce a different date than expected if the server's time zone differs from what the application assumes. Passing an explicit ZoneId to LocalDate.now(zoneId) removes that ambiguity.

Why does calling a method like plusDays() not change the original object?

Because every class in java.time is immutable by design — no method on LocalDate, LocalDateTime, or any related class ever modifies the object it is called on. Every such method returns a new instance representing the result, and the original object is always left exactly as it was.

What exception is thrown when parsing an invalid date string?

DateTimeParseException, an unchecked exception thrown by LocalDate.parse(), LocalDateTime.parse(), and similar parsing methods when the input string does not match the expected format or represents an invalid date.

Is java.time part of core Java, or does it need a separate dependency?

It is part of core Java, included in the JDK since Java 8 with no separate dependency required. It was built directly into the language specification, unlike its predecessor Joda-Time, which was always a third-party library.

How do I convert between java.util.Date and the new java.time classes?

Date provides a toInstant() method that converts it directly into an Instant, and Date.from(instant) does the reverse. From an Instant, converting to a ZonedDateTime or LocalDateTime just needs a ZoneId supplied through atZone(), which is the standard bridge used whenever legacy code and java.time code need to interoperate.

Summary

java.time replaces one overloaded, mutable Date class with a set of narrow, immutable ones — LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Period, and Duration — each built to model exactly one concept correctly. The subscription trial example above is the pattern worth remembering: pick the narrowest type for what is actually being represented, treat every value as immutable, and be deliberate about which side of a date boundary counts as "still valid."

Every method that looks like it changes a date returns a new one instead, == compares identity rather than the calendar value, and a mismatched formatter pattern only fails once real input reaches it — three habits worth internalizing before moving on to the dedicated articles on LocalDate, Period, Duration, and ZonedDateTime that build directly on the foundation covered here.

What to Read Next