Manipulating Dates & Times
The java.time API is immutable by design. You never modify a LocalDate in place; every manipulation returns a brand-new object. This lesson covers the three families of manipulation methods — plus/minus, with, and TemporalAdjusters — and explains when each is the right tool.
Why Immutability Matters Here
Before java.time, the old java.util.Date and Calendar were mutable, which caused hard-to-track bugs: methods mutated shared state, and objects passed between threads needed synchronisation. The new API solves both problems. The trade-off is verbosity — every operation produces a new value — but modern JVMs optimise short-lived objects extremely well, so the performance cost is negligible in practice.
Key mental model: treat LocalDate, LocalDateTime, ZonedDateTime, and their siblings exactly like String. You never change a String; you produce a new one. Same here.
plus and minus — Moving Along the Timeline
The most common need is "give me a date N days/months/years from now". Every Temporal implementation exposes typed convenience methods and a generic form that accepts a TemporalAmount.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.Duration;
public class PlusMinusDemo {
public static void main(String[] args) {
LocalDate today = LocalDate.of(2026, 6, 9);
// typed convenience methods
LocalDate nextWeek = today.plusDays(7);
LocalDate nextMonth = today.plusMonths(1);
LocalDate nextYear = today.plusYears(1);
LocalDate lastWeek = today.minusDays(7);
LocalDate lastQuarter = today.minusMonths(3);
System.out.println("Today: " + today);
System.out.println("Next week: " + nextWeek);
System.out.println("Next month: " + nextMonth);
System.out.println("Next year: " + nextYear);
System.out.println("Last week: " + lastWeek);
System.out.println("Last quarter: " + lastQuarter);
// generic form with a TemporalAmount (Period or Duration)
LocalDate in90Days = today.plus(Period.ofDays(90));
System.out.println("In 90 days: " + in90Days);
// DateTime — plus supports Duration too
LocalDateTime now = LocalDateTime.of(2026, 6, 9, 14, 30);
LocalDateTime soon = now.plusHours(2).plusMinutes(45);
System.out.println("Soon: " + soon);
}
}
Period models a date-based amount (years, months, days). Duration models a time-based amount (hours, minutes, seconds, nanos). You can pass either to the generic plus(TemporalAmount), but the receiving temporal must support the relevant fields — calling LocalDate.plus(Duration.ofHours(3)) throws an UnsupportedTemporalTypeException because a date has no hours.
Month-length surprises: LocalDate.of(2026, 1, 31).plusMonths(1) returns 2026-02-28, not 2026-03-03. Java.time clips to the last valid day of the target month (called end-of-month rule). This is the correct calendar behaviour, but if you later subtract the month back you may not get the original date. Design round-trip logic with this in mind.
with — Replacing a Specific Field
plus/minus are relative adjustments. with is an absolute replacement: "set the day-of-month to 1", "set the hour to 0", "set the year to 2030". It comes in typed overloads and a generic form.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoField;
public class WithDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 6, 15);
// typed overloads
LocalDate firstOfMonth = date.withDayOfMonth(1);
LocalDate lastDayOfYear = date.withDayOfYear(365); // non-leap 2026
LocalDate sameMonthDay2030 = date.withYear(2030);
System.out.println("First of month: " + firstOfMonth);
System.out.println("Last day of year: " + lastDayOfYear);
System.out.println("Same date in 2030: " + sameMonthDay2030);
// generic form using ChronoField
LocalDate withMonth = date.with(ChronoField.MONTH_OF_YEAR, 12);
System.out.println("Same day, December: " + withMonth);
// works on LocalDateTime too
LocalDateTime noon = LocalDateTime.of(2026, 6, 15, 9, 45)
.with(ChronoField.HOUR_OF_DAY, 12)
.with(ChronoField.MINUTE_OF_HOUR, 0);
System.out.println("Noon: " + noon);
}
}
The generic with(TemporalField, long) accepts any ChronoField constant, giving you fine-grained control — including niche fields like ALIGNED_WEEK_OF_MONTH or EPOCH_DAY. The typed overloads are preferred for clarity in most cases.
TemporalAdjusters — Business-Rule Adjustments
Many real-world needs cannot be expressed as "plus N units" or "set field to X": "the last Friday of this month", "the next business day", "the first Monday after a given date". The TemporalAdjusters factory class covers the common cases, and you can write your own.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.TemporalAdjusters;
public class AdjustersDemo {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 6, 9); // a Tuesday
LocalDate firstDay = date.with(TemporalAdjusters.firstDayOfMonth());
LocalDate lastDay = date.with(TemporalAdjusters.lastDayOfMonth());
LocalDate firstOfNext = date.with(TemporalAdjusters.firstDayOfNextMonth());
LocalDate nextFriday = date.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
LocalDate prevMonday = date.with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
LocalDate firstMonday = date.with(TemporalAdjusters.firstInMonth(DayOfWeek.MONDAY));
LocalDate lastFriday = date.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("First of month: " + firstDay);
System.out.println("Last of month: " + lastDay);
System.out.println("First of next month: " + firstOfNext);
System.out.println("Next Friday: " + nextFriday);
System.out.println("Previous Monday: " + prevMonday);
System.out.println("First Monday in June: " + firstMonday);
System.out.println("Last Friday in June: " + lastFriday);
}
}
Writing a Custom TemporalAdjuster
TemporalAdjuster is a functional interface with a single method adjustInto(Temporal). Because it is functional, you can implement it as a lambda. A classic example is a "next business day" adjuster that skips weekends.
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAdjuster;
public class BusinessDayAdjuster {
// skip Saturday and Sunday
public static final TemporalAdjuster NEXT_BUSINESS_DAY = (Temporal temporal) -> {
LocalDate date = LocalDate.from(temporal);
do {
date = date.plusDays(1);
} while (date.getDayOfWeek() == DayOfWeek.SATURDAY
|| date.getDayOfWeek() == DayOfWeek.SUNDAY);
return date;
};
public static void main(String[] args) {
LocalDate friday = LocalDate.of(2026, 6, 5); // Friday
LocalDate saturday = LocalDate.of(2026, 6, 6); // Saturday
LocalDate sunday = LocalDate.of(2026, 6, 7); // Sunday
System.out.println("After Friday: " + friday.with(NEXT_BUSINESS_DAY)); // Monday 2026-06-08
System.out.println("After Saturday: " + saturday.with(NEXT_BUSINESS_DAY)); // Monday 2026-06-08
System.out.println("After Sunday: " + sunday.with(NEXT_BUSINESS_DAY)); // Monday 2026-06-08
}
}
Compose adjusters for complex rules. A payment-due-date rule might be "last calendar day of the month, but if that is a weekend move to the previous Friday". Compose two adjusters: lastDayOfMonth() then your weekend-rollback adjuster. Keep each adjuster small and single-purpose.
Choosing the Right Tool
- Use plus/minus when moving a fixed amount along the timeline (7 days from now, 3 months earlier).
- Use with when you need to pin a specific field to a concrete value (first day of this month, midnight of this day).
- Use TemporalAdjusters when the rule is calendar- or business-logic-based and cannot be expressed as a fixed offset or field assignment.
Chaining Manipulations
Because every method returns a new object, you can chain calls fluently. Readability is the only limit.
import java.time.DayOfWeek;
import java.time.LocalDateTime;
import java.time.temporal.TemporalAdjusters;
public class ChainingDemo {
public static void main(String[] args) {
// "the start of the last business day of next month"
LocalDateTime result = LocalDateTime.now()
.plusMonths(1)
.with(TemporalAdjusters.lastDayOfMonth())
.with(TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY))
.withHour(9)
.withMinute(0)
.withSecond(0)
.withNano(0);
System.out.println(result);
}
}
previousOrSame vs previous: TemporalAdjusters.previousOrSame(DayOfWeek.FRIDAY) returns the date itself if it already is a Friday; previous(DayOfWeek.FRIDAY) always moves backwards at least one day. Pick the right variant so your rule handles the edge case correctly.
Summary
java.time manipulation is immutable and composable. plus/minus move along the timeline by a fixed amount; with pins a field to an absolute value; TemporalAdjusters express higher-level calendar rules and are extensible via the TemporalAdjuster functional interface. Together these three families cover virtually every date-manipulation scenario you will encounter in production code.