java.time (Java 8+) is the modern date and time API, and its objects are immutable. LocalDate is a calendar date with no time or zone, ideal for birthdays, deadlines and exam days. Months are 1 to 12 here, unlike older APIs. Arithmetic methods such as plusWeeks and plusMonths return a new date, and month arithmetic clamps sensibly: 31 January plus one month is 28 February.
ChronoUnit.DAYS.between(a, b) counts days; Period.between gives years, months and days, which is how you compute an age. DateTimeFormatter.ofPattern("dd/MM/yyyy") parses and formats dates the way people write them, and parsing is strict: 2026-02-31 throws DateTimeParseException instead of quietly becoming March. TemporalAdjusters find dates such as the first Monday of a month.
For a moment in time, use ZonedDateTime (a moment in a place, such as Africa/Dar_es_Salaam) or Instant (a moment in UTC, ideal for storing and logging). withZoneSameInstant shows the same moment in another zone. Duration measures elapsed time in hours, minutes and seconds.
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Locale;
public class Dates {
public static void main(String[] args) {
// A LocalDate is a calendar date with no time and no time zone. Months are 1-12.
LocalDate registered = LocalDate.of(2026, 3, 14);
LocalDate exams = LocalDate.parse("2026-11-02"); // ISO format
System.out.println(ChronoUnit.DAYS.between(registered, exams) + " days until exams"); // 233
// Objects are immutable: plusWeeks returns a new date
LocalDate deadline = registered.plusWeeks(6);
var pretty = DateTimeFormatter.ofPattern("EEEE d MMMM yyyy", Locale.UK);
System.out.println("Fee deadline: " + deadline.format(pretty)); // Saturday 25 April 2026
// Month arithmetic clamps to the end of the month
System.out.println(LocalDate.of(2026, 1, 31).plusMonths(1)); // 2026-02-28
// Age with Period: years, months and days between two dates
LocalDate born = LocalDate.parse("14/03/2008", DateTimeFormatter.ofPattern("dd/MM/yyyy"));
Period age = Period.between(born, LocalDate.of(2026, 10, 5));
System.out.printf("Age: %d years, %d months, %d days%n", age.getYears(), age.getMonths(), age.getDays());
// Strict parsing rejects dates that don't exist
try {
LocalDate.parse("2026-02-31");
} catch (DateTimeParseException e) {
System.out.println("Invalid: " + e.getParsedString());
}
// Adjusters find dates for you: the first Monday in July
LocalDate termStart = LocalDate.of(2026, 7, 1).with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
System.out.println("Term starts " + termStart + " (" + termStart.getDayOfWeek() + ")");
// Time zones: ZonedDateTime for a moment in a place, Instant for a moment in UTC
ZoneId eat = ZoneId.of("Africa/Dar_es_Salaam");
ZonedDateTime paid = ZonedDateTime.of(LocalDateTime.of(registered, LocalTime.of(9, 30)), eat);
Instant instant = paid.toInstant();
System.out.println(paid + " = " + instant); // ...+03:00[...] = ...06:30:00Z
System.out.println("In London: " + paid.withZoneSameInstant(ZoneId.of("Europe/London")).toLocalTime());
// Durations measure elapsed time
Duration exam = Duration.between(LocalTime.of(8, 0), LocalTime.of(10, 30));
System.out.println("Paper length: " + exam.toHours() + "h " + exam.toMinutesPart() + "m");
}
}Key points
LocalDatefor dates,ZonedDateTime/Instantfor moments; all are immutable.ChronoUnit.betweenandPeriod.betweenmeasure gaps;plusMonthsclamps to the month's end.DateTimeFormatterparses strictly and formats for people.
Exercise
Print a fee plan: split TSh 250,000 into 3 monthly instalments starting 31 January 2026, as whole shillings (the last one takes the remainder). Due dates that fall on a Saturday or Sunday move to the next Monday. Print each date like "Mon 2 Feb 2026" with the amount, then the total.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Each due date is computed from the original date with plusMonths(i). Adding one month at a time would drift: 31 Jan → 28 Feb → 28 Mar. nextSchoolDay uses an arrow switch on the DayOfWeek enum to push weekend dates to Monday. Money stays in a long of whole shillings, and the last instalment absorbs the remainder so the total is exact; %,d adds the thousands separators.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class FeePlan {
// Due dates that fall on a weekend move to the following Monday.
static LocalDate nextSchoolDay(LocalDate date) {
return switch (date.getDayOfWeek()) {
case SATURDAY -> date.plusDays(2);
case SUNDAY -> date.plusDays(1);
default -> date;
};
}
// Amounts are whole shillings in a long; the last instalment takes the remainder.
static void printPlan(long total, int count, LocalDate firstDue) {
var fmt = DateTimeFormatter.ofPattern("EEE d MMM yyyy", Locale.UK);
long base = total / count;
long sum = 0;
for (int i = 0; i < count; i++) {
long amount = i == count - 1 ? total - base * (count - 1) : base;
// Always add months to the ORIGINAL date, so 31 Jan -> 28 Feb -> 31 Mar (not 28 Mar)
LocalDate due = nextSchoolDay(firstDue.plusMonths(i));
System.out.printf("%-16s TSh %,9d%n", due.format(fmt), amount);
sum += amount;
}
System.out.printf("%-16s TSh %,9d%n", "Total", sum);
}
public static void main(String[] args) {
printPlan(250_000, 3, LocalDate.of(2026, 1, 31));
}
}