Why this matters
Optionalused as a replacement for every null is misuse; used as a return type it genuinely removes a class of bug. The distinction is routinely asked about.DateandCalendarare mutable, zero-indexed in places, and not thread-safe. Code still using them is a source of recurring bugs.- Time zones and daylight saving are where most date arithmetic goes wrong, and the new API makes the correct choice explicit.
Optional
// The signature now says that a user may not be found
public Optional<User> findByEmail(String email) {
return Optional.ofNullable(repository.lookup(email));
}
Optional.of(value); // throws if value is null — use when null is a bug
Optional.ofNullable(value); // empty if value is null — the usual factory
Optional.empty();
Reading the value
Optional<User> found = findByEmail(email);
// A default
User user = found.orElse(User.anonymous());
// A default computed only if needed
User lazy = found.orElseGet(() -> expensiveDefault());
// Fail if absent
User required = found.orElseThrow(() -> new UserNotFoundException(email));
User alsoRequired = found.orElseThrow(); // NoSuchElementException, Java 10+
// Act only if present
found.ifPresent(user -> audit.record(user));
found.ifPresentOrElse(user -> audit.record(user), () -> audit.missing(email));
// Transform without unwrapping
Optional<String> name = found.map(User::name);
Optional<Address> address = found.flatMap(User::address); // when the mapper returns an Optional
Optional<User> active = found.filter(User::isActive);
// Fall back to another Optional, Java 9+
Optional<User> either = found.or(() -> findByUsername(email));
// Treat it as a stream, Java 9+
List<User> users = emails.stream().map(this::findByEmail).flatMap(Optional::stream).toList();
How to use it, and how not to
| Aspect | Appropriate | Misuse |
|---|---|---|
| A method return type | Signals that absence is expected | A field — adds a wrapper object per instance |
| Chaining with map and filter | Avoids nested null checks | A method parameter — now three cases instead of two |
| orElseThrow for a required value | Clear and explicit | get() without checking — worse than a null check |
| ofNullable around a legacy API | Converts null at the boundary | Optional<List> — return an empty list instead |
| Replacing a null return | The intended use | Replacing every null in your codebase |
A method return type
AppropriateSignals that absence is expectedMisuseA field — adds a wrapper object per instanceChaining with map and filter
AppropriateAvoids nested null checksMisuseA method parameter — now three cases instead of twoorElseThrow for a required value
AppropriateClear and explicitMisuseget() without checking — worse than a null checkofNullable around a legacy API
AppropriateConverts null at the boundaryMisuseOptional<List> — return an empty list insteadReplacing a null return
AppropriateThe intended useMisuseReplacing every null in your codebase
Optional is for return types. Everywhere else it usually adds a case rather than removing one.
// The pattern Optional actually replaces
String city = null;
if (user != null) {
Address address = user.getAddress();
if (address != null) {
city = address.getCity();
}
}
// The same intent, flattened
String city = Optional.ofNullable(user)
.map(User::getAddress)
.map(Address::getCity)
.orElse("unknown");
The date-time API
Java 8 replaced Date and Calendar with a set of types that each model one concept.
Pick the type that matches the concept
LocalDate— a date with no time and no zone. A birthday, an invoice date.LocalTime— a time with no date. An opening hour.LocalDateTime— both, still with no zone. A wall-clock appointment.ZonedDateTime— a date and time in a specific zone, with daylight-saving rules applied.Instant— a point on the global timeline in UTC. What you store in a database.Duration— a time-based amount: seconds, minutes, hours.Period— a date-based amount: years, months, days.
LocalDate today = LocalDate.now();
LocalDate launch = LocalDate.of(2026, Month.MARCH, 15);
LocalTime opening = LocalTime.of(9, 30);
LocalDateTime meeting = LocalDateTime.of(launch, opening);
ZonedDateTime inTokyo = meeting.atZone(ZoneId.of("Asia/Tokyo"));
Instant now = Instant.now();
Everything is immutable
LocalDate date = LocalDate.of(2026, 1, 31);
LocalDate later = date.plusMonths(1); // 2026-02-28 — clamped, not invalid
LocalDate shifted = date.plusDays(10).minusWeeks(2);
LocalDate adjusted = date.withDayOfMonth(1);
System.out.println(date); // 2026-01-31 — unchanged
Every method returns a new object, so these types are thread-safe and safe to share. The clamping in the first line is worth noting: adding a month to 31 January gives the last valid day of February rather than throwing.
// Comparisons and queries
date.isBefore(later);
date.isAfter(launch);
date.getDayOfWeek(); // SATURDAY
date.isLeapYear();
date.lengthOfMonth();
// Adjusters for common relative dates
date.with(TemporalAdjusters.lastDayOfMonth());
date.with(TemporalAdjusters.next(DayOfWeek.MONDAY));
date.with(TemporalAdjusters.firstDayOfNextMonth());
Duration and Period
Duration meetingLength = Duration.ofMinutes(90);
Duration between = Duration.between(start, end);
long minutes = between.toMinutes();
Period age = Period.between(birthDate, LocalDate.now());
System.out.println(age.getYears() + " years, " + age.getMonths() + " months");
long days = ChronoUnit.DAYS.between(launch, today); // a single unit as a number
Duration measures time and is used with Instant and LocalTime. Period measures calendar amounts and is
used with LocalDate. The difference matters across a daylight-saving boundary: one day as a Period is the
same clock time tomorrow, while 24 hours as a Duration may be a different clock time.
Formatting and parsing
DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE;
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd MMM yyyy", Locale.UK);
String text = today.format(custom); // 01 Oct 2026
LocalDate parsed = LocalDate.parse("2026-10-01"); // ISO by default
LocalDate fromCustom = LocalDate.parse("01 Oct 2026", custom);
Migrating from Date and Calendar
Instant instant = legacyDate.toInstant();
Date back = Date.from(instant);
LocalDateTime local = legacyDate.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
LocalDate fromSql = sqlDate.toLocalDate();
| Aspect | Legacy | Modern |
|---|---|---|
| Mutability | Date and Calendar are mutable | Every type is immutable |
| Thread safety | SimpleDateFormat is not safe | DateTimeFormatter is |
| Month numbering | Calendar months start at 0 | January is 1, or use the Month enum |
| Concept separation | Date means instant, date and time at once | A distinct type per concept |
| Arithmetic | Manual field manipulation | plusDays, minusWeeks, TemporalAdjusters |
| Zones | Implicit and error-prone | Explicit in the type |
Mutability
LegacyDate and Calendar are mutableModernEvery type is immutableThread safety
LegacySimpleDateFormat is not safeModernDateTimeFormatter isMonth numbering
LegacyCalendar months start at 0ModernJanuary is 1, or use the Month enumConcept separation
LegacyDate means instant, date and time at onceModernA distinct type per conceptArithmetic
LegacyManual field manipulationModernplusDays, minusWeeks, TemporalAdjustersZones
LegacyImplicit and error-proneModernExplicit in the type
January being month 0 in Calendar is a defect that survived for fifteen years.
Common misreadings
- "
OptionaleliminatesNullPointerException." It eliminates null returns. An uncheckedget()reintroduces the same failure. - "
orElseis lazy." It always evaluates its argument.orElseGetis the lazy one. - "
Optionalis good for fields and parameters." It adds an object per instance and a third case for callers. Use it for return types. - "
Optional<List<T>>is good practice." Return an empty list. - "
LocalDateTimehas a time zone." It does not, which is why it is wrong for timestamps. - "
LocalDate.plusDaysmodifies the date." Every method returns a new object. - "
DurationandPeriodare interchangeable."Durationis time-based,Periodis calendar-based, and they differ across daylight saving. - "
Dateis fine if you are careful." It is mutable and its formatter is not thread-safe. Convert at the boundary.
Quick recall
Everything you need if you only revisit this box.
Optionalbelongs in return types, to make absence visible in the signature.ofthrows on null,ofNullableis the usual factory,emptyis the absent case.orElsealways evaluates;orElseGetis lazy. PreferorElseThrow()overget(), and never callget()unchecked.- Chain with
map,flatMap,filter,or,streaminstead of unwrapping early. - Date-time types, one per concept:
LocalDate,LocalTime,LocalDateTime,ZonedDateTime,Instant,Duration,Period. - Store an
Instantfor a moment; useLocalDatewhen the zone is irrelevant; useZonedDateTimewhen local rules matter. - Everything is immutable, so
plusDaysreturns a new value andDateTimeFormatteris a safestatic finalconstant. Durationis time-based,Periodis calendar-based — they differ across a daylight-saving boundary.
Test yourself
Answer these before moving on — recall is what makes it stick.