The java.time API: LocalDate, LocalTime and LocalDateTime
The modern date and time API is immutable, thread safe and clearly named, and it replaced the older Date and Calendar classes entirely.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Why java.time exists
The original java.util.Date and Calendar were mutable, not thread safe, and confusingly designed: months counted from zero, years counted from 1900, and a Date actually represented an instant rather than a date. Java 8 introduced java.time, which fixed all of it.
| Old | Modern replacement |
|---|---|
java.util.Date | Instant or LocalDateTime |
Calendar | LocalDate, ZonedDateTime |
SimpleDateFormat | DateTimeFormatter |
java.sql.Date | LocalDate |
Every type in java.time is immutable. Every method that appears to modify a value in fact returns a new one, which makes these types safe to share between threads and safe as map keys.Choosing the right type
| Type | Holds | Use for |
|---|---|---|
LocalDate | A date, no time, no zone | A birthday, an invoice date |
LocalTime | A time, no date, no zone | An opening time |
LocalDateTime | Date and time, no zone | A local appointment |
ZonedDateTime | Date, time and a zone | A meeting across regions |
OffsetDateTime | Date, time and a fixed offset | Timestamps in APIs and databases |
Instant | A point on the timeline, in UTC | Logging, measuring, storage |
Duration | A time based amount | Seconds, minutes, hours |
Period | A date based amount | Years, months, days |
Year, YearMonth, MonthDay | Partial dates | Card expiry, anniversaries |
Creating values
LocalDate today = LocalDate.now();
LocalDate independence = LocalDate.of(1947, 8, 15);
LocalDate typed = LocalDate.of(2026, Month.AUGUST, 21);
LocalDate parsed = LocalDate.parse("2026-08-21"); // ISO format by default
LocalTime opening = LocalTime.of(9, 30);
LocalTime precise = LocalTime.of(9, 30, 15, 500_000_000);
LocalDateTime meeting = LocalDateTime.of(2026, 8, 21, 14, 0);
LocalDateTime combined = today.atTime(opening);
LocalDate justTheDate = meeting.toLocalDate();Months are numbered from 1, as everyone always expected. The Month enum removes the ambiguity entirely.
Reading parts
LocalDate date = LocalDate.of(2026, 8, 21);
System.out.println(date.getYear()); // 2026
System.out.println(date.getMonth()); // AUGUST
System.out.println(date.getMonthValue()); // 8
System.out.println(date.getDayOfMonth()); // 21
System.out.println(date.getDayOfWeek()); // FRIDAY
System.out.println(date.getDayOfYear()); // 233
System.out.println(date.lengthOfMonth()); // 31
System.out.println(date.isLeapYear()); // falseArithmetic returns new values
LocalDate date = LocalDate.of(2026, 8, 21);
LocalDate nextWeek = date.plusWeeks(1);
LocalDate lastMonth = date.minusMonths(1);
LocalDate nextYear = date.plusYears(1);
date.plusDays(10); // result discarded, date is unchanged
LocalDate later = date.plusDays(10); // correctLocalDate endOfMonth = LocalDate.of(2026, 1, 31);
System.out.println(endOfMonth.plusMonths(1)); // 2026-02-28, clamped safelyAdding a month to 31 January gives the last valid day of February rather than an error or an overflow into March. The API resolves these cases predictably.
The with methods
LocalDate date = LocalDate.of(2026, 8, 21);
System.out.println(date.withDayOfMonth(1)); // 2026-08-01
System.out.println(date.withMonth(12)); // 2026-12-21
System.out.println(date.with(TemporalAdjusters.firstDayOfMonth()));
System.out.println(date.with(TemporalAdjusters.lastDayOfMonth()));
System.out.println(date.with(TemporalAdjusters.next(DayOfWeek.MONDAY)));
System.out.println(date.with(TemporalAdjusters.firstDayOfNextMonth()));Comparing
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 12, 31);
LocalDate today = LocalDate.now();
System.out.println(today.isAfter(start));
System.out.println(today.isBefore(end));
System.out.println(today.isEqual(start));
System.out.println(start.compareTo(end)); // negative
boolean inRange = !today.isBefore(start) && !today.isAfter(end);Note that isBefore and isAfter are strict. For an inclusive range, negate the opposite test as above.
A worked example
public record Subscription(LocalDate startedOn, int months) {
public LocalDate expiresOn() {
return startedOn.plusMonths(months);
}
public boolean isActiveOn(LocalDate date) {
return !date.isBefore(startedOn) && date.isBefore(expiresOn());
}
public long daysRemaining(LocalDate from) {
return Math.max(0, ChronoUnit.DAYS.between(from, expiresOn()));
}
}Subscription plan = new Subscription(LocalDate.of(2026, 1, 15), 12);
System.out.println(plan.expiresOn()); // 2027-01-15
System.out.println(plan.isActiveOn(LocalDate.now()));
System.out.println(plan.daysRemaining(LocalDate.now()));Measuring the gap
LocalDate birth = LocalDate.of(1998, 3, 12);
LocalDate today = LocalDate.now();
long days = ChronoUnit.DAYS.between(birth, today);
long months = ChronoUnit.MONTHS.between(birth, today);
int age = Period.between(birth, today).getYears();Common mistakes
- Discarding the result of
plusDaysand expecting the original to change. - Using
LocalDateTimefor a timestamp that must be comparable across zones. UseInstantorOffsetDateTime. - Reaching for
Date,CalendarorSimpleDateFormatin new code. - Assuming a day is always 24 hours, which fails across a daylight saving change.
- Comparing dates with
==rather thanequalsorisEqual.
Best practices
- Use
LocalDatewhen there is no time, and do not invent a midnight time. - Store timestamps as
Instantor in UTC, and convert to a zone only for display. - Use
MonthandDayOfWeekrather than raw integers. - Pass a
Clockinto code that needs the current time, so it can be tested. - Keep
java.timetypes in your domain model instead of converting to strings early.
public class InvoiceService {
private final Clock clock;
public InvoiceService(Clock clock) {
this.clock = clock;
}
public LocalDate dueDate() {
return LocalDate.now(clock).plusDays(30);
}
}
// In a test
InvoiceService service = new InvoiceService(
Clock.fixed(Instant.parse("2026-08-21T00:00:00Z"), ZoneOffset.UTC));Practice
- Calculate the number of days until the next new year from today.
- Why does
LocalDate.of(2026, 1, 31).plusMonths(1)return 28 February? - Write a method returning the first Monday of a given month.
- Explain when
LocalDateTimeis the wrong choice, with an example. - Make a method that uses the current date testable by injecting a
Clock.
Conclusion
Pick the type that matches exactly what you know: a date, a time, both, or a point on the timeline. Everything is immutable, so keep the returned value, and inject a Clock whenever the current time is involved.