Dates & Times (LocalDate, LocalDateTime)
Reviewed & published by Brayan K
Dates look simple and turn out to be a minefield — leap years, month lengths, time zones. Java's modern java.time package handles all of it with clean, immutable classes. This lesson covers the three you'll use daily: LocalDate, LocalTime, and LocalDateTime.
Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Before You Start
You'll need comfort with method calls and imports — everything here lives in java.time. Knowing that objects can be immutable will make the key idea click instantly.
What You'll Learn
- Create dates and times with LocalDate, LocalTime, LocalDateTime
- Why java.time objects are immutable (and the bug that causes)
- Add and subtract time with plus/minus methods
- Compare dates with isBefore, isAfter, isEqual
- Measure gaps with Period and ChronoUnit
- Format and parse with DateTimeFormatter
The Big Idea — A Stamped Calendar Page
💡 Analogy: A LocalDate is like a single page torn from a calendar — it says "17 June 2026" and nothing else. You can't erase and rewrite it; if you want "one week later" you grab a fresh page. That's immutability: every plusDays hands you a new page and leaves the original exactly as it was. This is what makes java.time safe to pass around without anyone secretly changing your dates.
There are three everyday building blocks: LocalDate (date only), LocalTime (time only), and LocalDateTime (both). "Local" means no time zone — perfect for birthdays, opening hours, and appointments where the zone is understood. We'll stick to these three.
1️⃣ Creating Dates and Times
Use the static of(...) factory methods to build a specific value, or now() for the current moment. Months are 1-based here (June is 6) — a welcome fix from the old Date class where January was 0. You can also use the readable Month.JUNE enum.
LocalDate.of(2026, 6, 17) // 17 June 2026
LocalDate.now() // today
LocalTime.of(14, 30) // 14:30
LocalDateTime.of(2026, 6, 17, 14, 30)Once you have an object, getters pull out the pieces: getYear(), getMonth(), getDayOfMonth(), getDayOfWeek(), and more.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.Month;
public class Main {
public static void main(String[] args) {
// Build a specific date (year, month, day)
LocalDate d = LocalDate.of(2026, 6, 17);
System.out.println("Date: " + d); // 2026-06-17
// Months can be named for clarity
LocalDate launch = LocalDate.of(2026, Month.DECEMBER, 25);
System.out.println("Launch: " + launch);
// A time of day
LocalTime t = LocalTime.of(14, 30, 0);
System.out.println("Time: " + t); // 14:30
// Date + time together
LocalDateTime dt = LocalDateTime.of(2026, 6, 17, 14, 30);
System.out.println("DateTime: " + dt); // 2026-06-17T14:30
// Pull pieces out
System.out.println("Year : " + d.getYear());
System.out.println("Month: " + d.getMonth() + " (" + d.getMonthValue() + ")");
System.out.println("Day : " + d.getDayOfMonth());
System.out.println("Weekday: " + d.getDayOfWeek());
}
}2️⃣ Immutability — the One Rule to Remember
Every java.time object is immutable. A method like plusDays(10) does not change the date — it returns a brand-new date and leaves the original untouched. This is wonderful for safety but trips up everyone at least once.
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 6, 17);
// java.time objects are IMMUTABLE. Every "change" returns a NEW object;
// the original is never touched.
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate inTenDays = today.plusDays(10);
System.out.println("today : " + today); // unchanged!
System.out.println("nextWeek : " + nextWeek);
System.out.println("lastMonth: " + lastMonth);
System.out.println("inTenDays: " + inTenDays);
// Methods chain because each returns a new LocalDate
LocalDate result = today.plusYears(1).minusDays(2).withDayOfMonth(1);
System.out.println("chained : " + result);
}
}3️⃣ Comparing and Measuring Gaps
Compare two dates with isBefore, isAfter, and isEqual. To measure the gap, you have two tools:
- Period.between(a, b) — a human breakdown into years, months, and days ("5 months, 16 days").
- ChronoUnit.DAYS.between(a, b) — a total count in one unit ("167 days"). Also MONTHS, YEARS, HOURS...
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 6, 17);
// Compare dates
System.out.println("start before end? " + start.isBefore(end));
System.out.println("end after start? " + end.isAfter(start));
System.out.println("same date? " + start.isEqual(end));
// Period = human-readable difference (years / months / days)
Period p = Period.between(start, end);
System.out.printf("Period: %d months, %d days%n",
p.getMonths(), p.getDays());
// ChronoUnit = total count in one unit
long totalDays = ChronoUnit.DAYS.between(start, end);
System.out.println("Total days between: " + totalDays);
}
}4️⃣ Formatting and Parsing
DateTimeFormatter converts in both directions: format turns a date object into a custom String, and parse turns a String back into a date object. You define the shape with a pattern of letters.
| Letter | Means | Example |
|---|---|---|
| yyyy | 4-digit year | 2026 |
| MM / MMMM | Month number / name | 06 / June |
| dd | Day of month | 17 |
| EEEE | Weekday name | Wednesday |
| HH:mm | 24-hour time | 14:05 |
| 'text' | Literal text | 'at' |
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) {
LocalDateTime dt = LocalDateTime.of(2026, 6, 17, 14, 5);
// FORMAT: object -> nicely formatted String
DateTimeFormatter human =
DateTimeFormatter.ofPattern("EEEE, d MMMM yyyy 'at' HH:mm");
System.out.println(dt.format(human));
DateTimeFormatter shortFmt =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
System.out.println(dt.format(shortFmt));
// PARSE: String -> object (the reverse)
LocalDateTime parsed =
LocalDateTime.parse("2026-12-25T09:00:00");
System.out.println("Parsed: " + parsed);
System.out.println("Parsed weekday: " + parsed.getDayOfWeek());
}
}🧩 Reorder Challenge
These lines compute and print how many days remain until a holiday. They've been shuffled.
A) long days = ChronoUnit.DAYS.between(today, holiday);
B) LocalDate today = LocalDate.of(2026, 6, 17);
C) System.out.println(days + " days until the holiday");
D) LocalDate holiday = LocalDate.of(2026, 12, 25);Correct order: B, D, A, C
LocalDate today = LocalDate.of(2026, 6, 17);
LocalDate holiday = LocalDate.of(2026, 12, 25);
long days = ChronoUnit.DAYS.between(today, holiday);
System.out.println(days + " days until the holiday");
// Output: 191 days until the holidayCreate both dates first, measure the gap with ChronoUnit.DAYS.between (earlier date first so the result is positive), then print.
🧠 Quick Recall
Predict the output before opening each answer.
LocalDate d = LocalDate.of(2026, 6, 17);
d.plusDays(5);
System.out.println(d);Answer: 2026-06-17 — unchanged. plusDays(5) returns a new date that is thrown away because the result isn't captured. LocalDate is immutable. To move the date you'd need d = d.plusDays(5);.
LocalDate a = LocalDate.of(2026, 1, 31);
LocalDate b = a.plusMonths(1);
System.out.println(b);Answer: 2026-02-28. There's no 31 February, so plusMonths clamps to the last valid day of the target month. (2026 isn't a leap year, so February ends on the 28th.) java.time handles month-length edge cases for you.
LocalDate start = LocalDate.of(2026, 6, 1);
LocalDate end = LocalDate.of(2026, 6, 17);
System.out.println(ChronoUnit.DAYS.between(start, end));Answer: 16. between counts whole days from start to end — June 17 minus June 1 is 16 days. It's exclusive of the start and inclusive of the end's calendar position, so the gap, not the number of dates, is 16.
🎯 Your Turn #1 — 18th birthday
Given a birth date, compute and print the 18th-birthday date.
import java.time.LocalDate;
public class Main {
public static void main(String[] args) {
// YOUR TURN: given a person's birth date, compute the date of their
// 18th birthday and print it. (Hint: plusYears.)
// Expected output: 18th birthday: 2026-04-09
LocalDate birth = LocalDate.of(2008, 4, 9);
LocalDate eighteenth = birth; // fix this line
System.out.println("18th birthday: " + eighteenth);
}
}🎯 Your Turn #2 — Countdown
Count the days between today and a deadline using ChronoUnit.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class Main {
public static void main(String[] args) {
// YOUR TURN: count how many days until a deadline.
// Use ChronoUnit.DAYS.between(...).
// Expected output: Days until deadline: 14
LocalDate today = LocalDate.of(2026, 6, 17);
LocalDate deadline = LocalDate.of(2026, 7, 1);
long daysLeft = 0; // compute this
System.out.println("Days until deadline: " + daysLeft);
}
}🧩 Mini-Challenge — Age calculator
Write an age method that returns whole years between a birth date and today using Period.
import java.time.LocalDate;
import java.time.Period;
public class Main {
// GOAL: write age(birth, today) that returns a person's age in whole years.
// Then write a formatted line. Use Period.between(...).getYears().
//
// Expected output:
// Born 1990-02-15, today 2026-06-17 -> age 36
// Born 2010-12-01, today 2026-06-17 -> age 15
static int age(LocalDate birth, LocalDate today) {
// return the whole years between birth and today
return 0; // your code here
}
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 6, 17);
LocalDate a = LocalDate.of(1990, 2, 15);
LocalDate b = LocalDate.of(2010, 12, 1);
System.out.println("Born " + a + ", today " + today + " -> age " + age(a, today));
System.out.println("Born " + b + ", today " + today + " -> age " + age(b, today));
}
}Common Errors
- ❌ Ignoring the return value. date.plusDays(1); changes nothing. Fix: assign it: date = date.plusDays(1);.
- ❌ Confusing MM and mm in patterns. mm gives minutes, not months. Fix: use uppercase MM for month, lowercase mm for minutes.
- ❌ Reaching for old Date/Calendar. They're mutable and 0-based for months. Fix: always use java.time in new code.
- ❌ Invalid date values. LocalDate.of(2026, 13, 1) throws DateTimeException. Fix: month is 1–12, day must exist in that month.
- ❌ Parse format mismatch. Parsing "17/06/2026" with the default ISO parser throws. Fix: pass a matching DateTimeFormatter to parse.
- ❌ Confusing Period and Duration. Period is for date units (days/months/years), Duration for time units (hours/seconds). Mixing them won't give the value you expect.
Pro Tips
- 💡 Use LocalDate for dates without a time (birthdays, deadlines) — don't drag a time of 00:00 around if you don't need it.
- 💡 Reach for ZonedDateTime or Instant the moment time zones matter (servers, global users). Local* types deliberately ignore zones.
- 💡 Store an instant, format on display. Keep the raw date/time in your data; only apply a DateTimeFormatter when showing it to a human.
- 💡 Immutability makes these classes thread-safe — you can share one DateTimeFormatter across threads, unlike the old SimpleDateFormat.
📋 Quick Reference
| Task | Syntax | Notes |
|---|---|---|
| Specific date | LocalDate.of(2026, 6, 17) | Month is 1-based |
| Today / now | LocalDate.now() | Current date |
| Add time | d = d.plusDays(7) | Returns a new object |
| Compare | a.isBefore(b) | Also isAfter, isEqual |
| Days between | ChronoUnit.DAYS.between(a, b) | Total count, one unit |
| Human gap | Period.between(a, b) | Years/months/days |
| Format | d.format(formatter) | DateTimeFormatter.ofPattern |
🎉 Lesson Complete!
You can now build dates and times with java.time, you understand why every object is immutable (and the bug that causes), and you can add, compare, measure, format, and parse with confidence. You're using the same modern API every professional Java codebase relies on.
Practice quiz
In LocalDate.of(2026, 6, 17), how is the month value interpreted?
- 0-based, so 6 is July
- It is the day, not the month
- 1-based, so 6 is June
- It must be a Month enum
Answer: 1-based, so 6 is June. java.time months are 1-based: 6 means June. This fixes the old Date class where January was 0.
After LocalDate d = LocalDate.of(2026,6,17); d.plusDays(5); what does d hold?
- 2026-06-17
- 2026-06-22
- It throws an exception
- null
Answer: 2026-06-17. java.time objects are immutable; plusDays returns a NEW date that is discarded here, so d is unchanged. Verified on Java 21.
What does LocalDate.of(2026, 1, 31).plusMonths(1) produce?
- 2026-02-31
- 2026-03-03
- It throws DateTimeException
- 2026-02-28
Answer: 2026-02-28. There is no 31 February, so plusMonths clamps to the last valid day; 2026 is not a leap year, giving 2026-02-28. Verified on Java 21.
Which class should you use for a date with no time, like a birthday?
- LocalDateTime
- LocalDate
- LocalTime
- Instant
Answer: LocalDate. LocalDate holds a date with no time-of-day, perfect for birthdays and deadlines.
What does ChronoUnit.DAYS.between(LocalDate.of(2026,6,1), LocalDate.of(2026,6,17)) return?
- 16
- 17
- 15
- 1
Answer: 16. between counts whole days from start to end: June 17 minus June 1 is 16. Verified on Java 21.
Which pair measures a human breakdown of years/months/days versus a total count in one unit?
- Duration vs Period
- Instant vs LocalDate
- Period vs ChronoUnit.DAYS.between
- ChronoUnit vs LocalTime
Answer: Period vs ChronoUnit.DAYS.between. Period.between gives a years/months/days breakdown; ChronoUnit.DAYS.between gives a single total count of days.
In a DateTimeFormatter pattern, what is the difference between MM and mm?
- MM is minutes, mm is month
- MM is month, mm is minutes
- Both mean month
- Both mean minutes
Answer: MM is month, mm is minutes. Pattern letters are case-sensitive: uppercase MM is the month, lowercase mm is minutes — a classic source of bugs.
Why prefer java.time over the old Date and Calendar classes?
- java.time is mutable and faster
- Date and Calendar are immutable and thread-safe
- Date supports time zones and java.time does not
- java.time classes are immutable, thread-safe, and use 1-based months
Answer: java.time classes are immutable, thread-safe, and use 1-based months. The modern java.time API is immutable, thread-safe, clearly named, and uses natural 1-based months, unlike the error-prone legacy classes.
What is the correct way to actually move a date forward by one day?
- date.plusDays(1);
- date = date.plusDays(1);
- date.addDays(1);
- date.setDay(date.getDay()+1);
Answer: date = date.plusDays(1);. Because dates are immutable you must capture the returned value: date = date.plusDays(1).
Period measures date-based units; what does Duration measure?
- Years, months, and days
- Only whole days
- Hours, minutes, seconds, and nanos
- Time zones
Answer: Hours, minutes, seconds, and nanos. Duration measures time-based units (hours/minutes/seconds/nanos) and pairs with LocalTime/Instant, while Period covers years/months/days.
Continue this course
- Previous: Packages & Imports
- Next: Interfaces and Abstract Classes — Define contracts with interfaces and share behaviour with abstract classes
- Quick reference: Java cheat sheet
Frequently asked questions
Why use java.time instead of the old Date and Calendar classes?
The old java.util.Date and Calendar are notoriously error-prone: they're mutable (so they can be changed behind your back), Date months are 0-based (January is 0), and the API is confusing. The modern java.time package (added in Java 8) fixes all of this — its classes are immutable, thread-safe, clearly named, and use natural values (June is month 6). Always reach for java.time in new code.
What's the difference between LocalDate, LocalTime, and LocalDateTime?
LocalDate is a date with no time (2026-06-17) — use it for birthdays or deadlines. LocalTime is a time with no date (14:30) — use it for opening hours. LocalDateTime combines both (2026-06-17T14:30) — use it for timestamps within a known zone. 'Local' means no time zone is attached; when you need zone awareness use ZonedDateTime, and for a universal machine timestamp use Instant.
Why didn't my date change after calling plusDays?
Because java.time objects are immutable. Methods like plusDays, minusMonths, and withYear never modify the object — they return a brand-new object with the change applied. You must capture the result: write date = date.plusDays(1), not just date.plusDays(1). Ignoring the return value is the single most common java.time mistake.
What's the difference between Period and Duration?
Period measures time in date-based units — years, months, and days — and is paired with LocalDate. Duration measures time in time-based units — hours, minutes, seconds, nanos — and is paired with LocalTime/Instant. So 'how many months until launch' is a Period, while 'how many seconds did the task take' is a Duration. For a total count in a single unit (e.g. total days), use ChronoUnit.DAYS.between(...).
How do I turn a date into a custom string and back?
Use DateTimeFormatter. To format, build a formatter with a pattern and call date.format(formatter): DateTimeFormatter.ofPattern("dd/MM/yyyy"). To parse a string back, call the type's parse method, optionally with a formatter: LocalDate.parse("17/06/2026", formatter). Pattern letters are case-sensitive — MM is month, mm is minutes — which trips people up constantly.