Java compares two dates with the isBefore(), isAfter() and isEqual() methods of LocalDate, LocalDateTime and ZonedDateTime, and with compareTo() when we need an order for sorting. Each method compares two values of the same class and answers the question in its name.
We compare dates whenever a business rule depends on the calendar, such as a library loan that is returned after its due date, a coupon that expires, or a booking that must fall inside a date range.
The following example compares the due date and the return date of a library book, with the result of each line as a comment.
LocalDate due = LocalDate.of(2024, 5, 10);
LocalDate returned = LocalDate.of(2024, 5, 14);
boolean late = returned.isAfter(due); // true
boolean early = returned.isBefore(due); // false
boolean onTime = returned.isEqual(due); // false
int order = returned.compareTo(due); // 4
LocalDateTime pickedUp = LocalDateTime.of(2024, 5, 10, 9, 0);
LocalDateTime closed = LocalDateTime.of(2024, 5, 10, 18, 30);
boolean sameMoment = pickedUp.isEqual(closed); // false
boolean sameDay = pickedUp.toLocalDate().isEqual(closed.toLocalDate()); // true
Notice that a LocalDateTime comparison includes the time, so we call toLocalDate() when only the calendar day matters. The sections that follow compare each java.time class, explain why ZonedDateTime needs extra care, check date ranges and sorting, and convert legacy Date and Calendar values before comparing them.
1. Date Comparison since Java 8
The java.time classes have comparison methods that read like the question we ask, so a comparison needs no arithmetic. Each class compares only with values of its own kind, so the compiler rejects a call such as localDate.isBefore(localDateTime).

Before writing the comparison, we decide what the rule cares about. A due date cares about the day, a parking ticket cares about the minute, and a meeting shared across countries cares about the moment on the timeline.
1.1. Core Classes
The date classes in Java 8 and later differ in how much information they hold, and that decides what a comparison checks.
| Class | Holds | Comparison checks |
|---|---|---|
| LocalDate | Only the date, without time and time zone | The date |
| LocalDateTime | Date and time, without time zone | The date and the time |
| ZonedDateTime | Date and time with a time zone | The instant for isXxx(), every field for equals() and compareTo() |
| Instant | Seconds and nanoseconds since 1970-01-01T00:00Z | The moment on the timeline |
1.2. How to Compare Dates
All four classes have these methods, except that Instant has no isEqual() and uses equals() for the same check. Each method throws a NullPointerException when the argument is null.
- The call date1.isAfter(date2) returns true when date1 comes after date2, or false otherwise.
- The call date1.isBefore(date2) returns true when date1 comes before date2, or false otherwise.
- The call date1.isEqual(date2) returns true when both values stand for the same date or moment.
- The call date1.compareTo(date2) returns either a negative number, zero or a positive number, which is what sorting needs.
We check the sign of compareTo(), never a fixed value such as 1 or -1. For LocalDate, the method returns the difference of the first field that differs, so dates four days apart in one month give 4, and dates two years apart give 2.
LocalDate due = LocalDate.of(2024, 5, 10);
LocalDate returned = LocalDate.of(2024, 5, 14);
int days = returned.compareTo(due); // 4
int years = LocalDate.of(2026, 1, 1).compareTo(due); // 2
String result = days > 0 ? "late" : days < 0 ? "early" : "on time"; // "late"
The value of compareTo() is not a day count. When we need the number of days between the two dates, we use ChronoUnit.DAYS.between(due, returned), which returns 4, as the article on counting days between two dates shows. The methods for comparing LocalDate values have their own article with more cases.
1.3. Comparing Dates Without the Time Part
A LocalDateTime and a ZonedDateTime compare the time as well, so two values on the same calendar day are not equal when the hours differ. If the rule is about days, we compare the toLocalDate() results. If the rule is about a coarser time unit, such as the hour, we cut off the smaller fields with truncatedTo().
LocalDateTime pickedUp = LocalDateTime.of(2024, 5, 10, 9, 0);
LocalDateTime closed = LocalDateTime.of(2024, 5, 10, 18, 30);
boolean sameDay = pickedUp.toLocalDate().isEqual(closed.toLocalDate()); // true
boolean before = pickedUp.isBefore(closed); // true
LocalDateTime scan = LocalDateTime.of(2024, 5, 10, 9, 42, 15);
boolean sameHour = pickedUp.truncatedTo(ChronoUnit.HOURS).isEqual(scan.truncatedTo(ChronoUnit.HOURS)); // true
For more cases with timestamps, such as ignoring seconds, read comparing LocalDateTime instances.
1.4. Comparing ZonedDateTime Instances
A ZonedDateTime holds an instant and a zone, and the methods disagree about which of the two counts. The methods isBefore(), isAfter() and isEqual() compare only the instant. The methods compareTo() compares the instant first, and on a tie it compares the local date-time, the zone ID and the chronology. The method equals() returns true only when the local date-time, the offset and the zone are all the same.
For example, a video call app stores a meeting created by a user in India and shows the same meeting to a user in New York. Both values stand for one moment, so a clash check must treat them as equal.
ZonedDateTime india = ZonedDateTime.of(2024, 4, 26, 9, 0, 0, 0, ZoneId.of("Asia/Kolkata"));
ZonedDateTime newYork = india.withZoneSameInstant(ZoneId.of("America/New_York")); // 2024-04-25T23:30-04:00[America/New_York]
boolean sameInstant = india.isEqual(newYork); // true
boolean equalObjects = india.equals(newYork); // false
int order = india.compareTo(newYork); // 1
int instantOrder = india.toInstant().compareTo(newYork.toInstant()); // 0
boolean sameLocalDate = india.toLocalDate().isEqual(newYork.toLocalDate()); // false
The last line shows that the same day depends on the zone, because the meeting is on April 26 in India and on April 25 in New York. To compare the date part of two ZonedDateTime values, we convert both to one zone with withZoneSameInstant() and call toLocalDate(). Daylight saving overlaps add one more trap, which the article on ZonedDateTime comparison covers.
1.5. Checking if a Date Is Between Two Dates
The java.time classes have no isBetween() method, so we combine two checks. For an inclusive range, where the start and end dates count as inside, we write !date.isBefore(start) && !date.isAfter(end). The form isAfter(start) && isBefore(end) gives an exclusive range, which leaves out the first and the last day.
For example, a library runs a summer reading program from May 1 to May 31, and every book returned on one of those days earns a point.
LocalDate start = LocalDate.of(2024, 5, 1);
LocalDate end = LocalDate.of(2024, 5, 31);
LocalDate returned = LocalDate.of(2024, 5, 31);
boolean inclusive = !returned.isBefore(start) && !returned.isAfter(end); // true
boolean exclusive = returned.isAfter(start) && returned.isBefore(end); // false
A half-open range, with an inclusive start and an exclusive end, suits timestamps best, because the end of one slot is the start of the next and no moment belongs to two slots.
1.6. Sorting Dates and Finding the Latest Date
All java.time classes implement Comparable, so a list of dates sorts in time order with sorted() or List.sort(null). The methods Collections.max() and Stream.min() use the same order to find the latest and the earliest date. For sorting objects by a date field, we build a Comparator with Comparator.comparing().
List<LocalDate> dates = List.of(LocalDate.of(2024, 5, 14), LocalDate.of(2024, 3, 2), LocalDate.of(2024, 8, 20));
List<LocalDate> oldestFirst = dates.stream().sorted().toList(); // [2024-03-02, 2024-05-14, 2024-08-20]
List<LocalDate> newestFirst = dates.stream().sorted(Comparator.reverseOrder()).toList(); // [2024-08-20, 2024-05-14, 2024-03-02]
LocalDate latest = Collections.max(dates); // 2024-08-20
LocalDate earliest = dates.stream().min(Comparator.naturalOrder()).orElseThrow(); // 2024-03-02
A loan that is not returned yet has no return date, so a list of return dates can contain null. Sorting such a list with the natural order throws a NullPointerException, so we wrap the comparator in Comparator.nullsLast() and check for null before a single comparison.
List<LocalDate> returnDates = new ArrayList<>(Arrays.asList(LocalDate.of(2024, 5, 14), null, LocalDate.of(2024, 3, 2)));
returnDates.sort(Comparator.nullsLast(Comparator.naturalOrder()));
String sortedText = returnDates.toString(); // "[2024-03-02, 2024-05-14, null]"
LocalDate notReturned = null;
boolean unsafe = returnDates.get(0).isBefore(notReturned); // NullPointerException
boolean safe = notReturned != null && returnDates.get(0).isBefore(notReturned); // false
2. Comparing Legacy Date and Calendar Objects
The classes java.util.Date and Calendar still come from JDBC drivers and older library APIs. Both always hold a moment with milliseconds, so their methods before(), after(), equals() and compareTo() always compare the time as well.
Date date1 = Date.from(Instant.parse("2024-05-10T09:00:00Z"));
Date date2 = Date.from(Instant.parse("2024-05-10T18:30:00Z"));
boolean isEqual = date1.equals(date2); // false
boolean isBefore = date1.before(date2); // true
int order = date1.compareTo(date2); // -1
To compare only the date part, we convert each value to a LocalDate in a zone that we choose and compare the results. The calendar day of a Date depends on the zone, so the same date2 falls on May 10 in UTC and on May 11 in India.
Date date1 = Date.from(Instant.parse("2024-05-10T09:00:00Z"));
Date date2 = Date.from(Instant.parse("2024-05-10T18:30:00Z"));
ZoneId zone = ZoneId.of("UTC");
LocalDate day1 = date1.toInstant().atZone(zone).toLocalDate(); // 2024-05-10
LocalDate day2 = date2.toInstant().atZone(zone).toLocalDate(); // 2024-05-10
boolean sameDate = day1.isEqual(day2); // true
LocalDate kolkataDay2 = date2.toInstant().atZone(ZoneId.of("Asia/Kolkata")).toLocalDate(); // 2024-05-11
A Calendar converts the same way with calendar.toInstant(), or with ((GregorianCalendar) calendar).toZonedDateTime(), which keeps the calendar’s own zone. The older trick of comparing Calendar.YEAR and Calendar.DAY_OF_YEAR works too, but it uses the default zone of the JVM without saying so. More conversions are in the article on converting between Date and LocalDate.
3. Deciding the Status of a Library Loan
A library app shows a status next to every loan. The rule needs the due date, the return date, which is null while the book is out, and today’s date in the library’s zone. We pass today’s date as a parameter, so the method gives the same answer in a test on any day.
static String loanStatus(LocalDate due, LocalDate returned, LocalDate today) {
if (returned == null) {
return today.isAfter(due) ? "overdue" : "on loan";
}
return returned.isAfter(due) ? "returned late" : "returned on time";
}
LocalDate due = LocalDate.of(2024, 5, 10);
String onLoan = loanStatus(due, null, LocalDate.of(2024, 5, 9)); // "on loan"
String overdue = loanStatus(due, null, LocalDate.of(2024, 5, 11)); // "overdue"
String onTime = loanStatus(due, LocalDate.of(2024, 5, 10), LocalDate.of(2024, 6, 1)); // "returned on time"
String lateReturn = loanStatus(due, LocalDate.of(2024, 5, 14), LocalDate.of(2024, 6, 1)); // "returned late"
LocalDate todayInLibrary = LocalDate.now(ZoneId.of("Europe/London")); // the current date in London
A book returned on the due date counts as on time, because isAfter() is false for equal dates. In production, the caller passes LocalDate.now(zone) with the library’s zone, so a server running in UTC does not mark a loan as overdue a few hours early.
4. Date Comparison FAQs
The == operator, mixed date classes and the current date cause most comparison bugs.
4.1. Can we compare dates with ==?
No. The operator == checks whether two variables point to the same object, so two LocalDate objects for the same day can give false. We use isEqual() or equals().
LocalDate a = LocalDate.of(2024, 5, 10);
LocalDate b = LocalDate.parse("2024-05-10");
boolean sameObject = a == b; // false
boolean sameDate = a.isEqual(b); // true
4.2. How do we compare a LocalDate with a LocalDateTime?
We convert one value to the type of the other. Either we call toLocalDate() on the LocalDateTime and compare dates, or we call atStartOfDay() on the LocalDate and compare date-times. The direct call localDate.isBefore(localDateTime) does not compile, because isBefore() expects a ChronoLocalDate.
LocalDate due = LocalDate.of(2024, 5, 10);
LocalDateTime pickedUp = LocalDateTime.of(2024, 5, 10, 9, 0);
boolean sameDay = pickedUp.toLocalDate().isEqual(due); // true
boolean afterMidnight = due.atStartOfDay().isBefore(pickedUp); // true
4.3. How do we check if a date is today, in the past or in the future?
We compare it with LocalDate.now(zone). The zone matters, because at 23:00 in New York it is already the next day in London. The article on the current date and time shows the options for reading the clock.
4.4. Why does Date.equals() give a different result for a Timestamp?
The class java.sql.Timestamp extends Date but overrides equals(), which returns false for any argument that is not a Timestamp. So date.equals(timestamp) can return true while timestamp.equals(date) returns false for the same moment. We compare with compareTo(), or convert both values with toInstant().
5. Conclusion
In Java 8 and later, we compare dates with isBefore(), isAfter() and isEqual(), and use compareTo() only for ordering, checking its sign and not its value.
The class decides what counts as equal. A LocalDate compares the day, a LocalDateTime also compares the time, and for a ZonedDateTime, only the isXxx() methods ignore the zone. Legacy Date and Calendar values become LocalDate or Instant values first, in a zone we pick. The Java date and time tutorial links the articles on related operations, such as adding days and parsing dates.
6. References
- LocalDate (Java SE 25 API)
- LocalDateTime (Java SE 25 API)
- ChronoZonedDateTime (Java SE 25 API)
- Date (Java SE 25 API)
- Timestamp (Java SE 25 API)
Happy Learning !!