Java · Java 8 Features

Java 8: Optional, java.time and the Rest

Optional as a return type and its eager orElse, java.time's immutable types and what DST does to a day, thread-safe formatting, and the platform changes: Metaspace, HashMap tree bins and the new Map methods.

  • 6 min read
  • 14 practice questions
  • 1 graded problem
On this page
  1. Description
  2. Optional
  3. The one Optional mistake almost everyone makes
  4. java.time: picking the right type
  5. Daylight saving time: when a day is 23 hours
  6. The new Map methods, and the rest of Java 8
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

Besides lambdas and streams, Java 8 added a set of everyday tools that backend code uses constantly. Optional makes "there might be no result" visible in a method's signature. java.time replaced the old, error-prone Date and Calendar with clear types for moments, dates and durations. And Map gained helper methods such as computeIfAbsent and merge.

Each of these has one or two rules that cause real bugs: a fallback that runs when it shouldn't, a "daily" job that drifts by an hour twice a year, a grouping map that stays empty. They're also favorite interview questions, because they show whether someone has used the API or only read about it.

Here's the path, from easiest to most technical:

  1. Optional: returning "maybe nothing" safely.
  2. The one Optional mistake almost everyone makes.
  3. java.time: which type to use for which kind of time.
  4. Daylight saving time, and why "a day" can mean 23 hours.
  5. The new Map methods, and the rest of Java 8.

Optional

A method that might not find anything used to return null, and callers had to remember to check. Optional<T> puts that possibility in the method's signature:

Optional<User> findUser(long id);

The caller then has to decide what happens when it's empty:

String name = findUser(id).map(User::name).orElse("unknown");     // a fallback value
User user = findUser(id).orElseThrow(() -> new UserNotFound(id));  // an error
findUser(id).ifPresent(u -> mailer.welcome(u));                    // only if found

A few rules:

  • Create it with Optional.ofNullable(x) when x might be null; Optional.of(null) throws a NullPointerException. Optional.empty() is the empty box.
  • Avoid get(). On an empty Optional it throws NoSuchElementException: No value present, and if (opt.isPresent()) use(opt.get()); is just a null check with extra steps. Use map, ifPresent, orElse or orElseThrow.
  • flatMap for functions that return an Optional. If Customer::address returns Optional<Address>, map would give you an Optional<Optional<Address>>; flatMap flattens it.
  • Use it as a return type. Not for fields, not for method parameters, and never Optional<List<T>>: return an empty list instead.

The one Optional mistake almost everyone makes

A sign-in flow looks up a user and falls back to a guest account:

User user = findUser(id).orElse(createGuestAccount());

Users are always found, yet the guest-account table grows by one row per sign-in. orElse takes a value, so Java runs createGuestAccount() first, to have the value ready, every single time, before it even looks inside the Optional.

orElseGet takes a function and only calls it when the Optional is empty:

User user = findUser(id).orElseGet(this::createGuestAccount);   // only creates a guest when needed

Use orElse for constants and cheap values, and orElseGet for anything that costs something or has side effects.

java.time: picking the right type

Java's old Date and Calendar classes were mutable, not thread-safe, and numbered months from 0. java.time fixed all of that, and gives you a separate type for each kind of time:

TypeWhat it isUse it for
Instantone exact moment, the same everywheretimestamps: "order placed", log events. Store and compare these
LocalDatea date with no time or zonebirthdays, due dates, holidays
LocalDateTimea date and time with no zonerarely right on a server: ambiguous until you add a zone
ZonedDateTimea date and time in a specific zoneshowing times to users, scheduling at someone's local time
Durationan exact amount of time (seconds)timeouts, "retry after 30 seconds"
Periodan amount of calendar time (days, months, years)"renews in 1 month"

All these types are immutable: methods that "change" a value return a new one, and the original stays as it was.

LocalDate due = LocalDate.of(2026, 3, 31);
due.plusDays(1);              // returns 2026-04-01, but nobody keeps it: due is unchanged
due = due.plusMonths(1);      // 2026-04-30: April has no 31st, so it's clamped to the last day

For formatting and parsing, use DateTimeFormatter, which is immutable and safe to share as a constant. The old SimpleDateFormat isn't thread-safe: one shared instance used by several threads produces wrong dates or exceptions.

Daylight saving time: when a day is 23 hours

In New York, clocks jumped forward from 2:00 to 3:00 on March 8, 2026. Starting from noon the day before, two ways of adding "one day" disagree:

ZonedDateTime noon = ZonedDateTime.of(2026, 3, 7, 12, 0, 0, 0, ZoneId.of("America/New_York"));
noon.plus(Duration.ofDays(1));    // 2026-03-08T13:00: exactly 24 hours later
noon.plusDays(1);                 // 2026-03-08T12:00: the same wall-clock time, only 23 hours later

Duration counts exact seconds; plusDays and Period count calendar days and keep the local time. Neither is wrong; they answer different questions. A job that runs "every 24 hours" and a job that runs "every day at noon" are different jobs, and twice a year the difference shows. Times that fall in the skipped hour don't exist (2:30 that morning is moved to 3:30), so be careful with anything scheduled between 2 and 3 a.m.

The new Map methods, and the rest of Java 8

Java 8 gave Map several methods that replace common check-then-act code:

counts.merge(word, 1, Integer::sum);                              // count
byCity.computeIfAbsent(city, k -> new ArrayList<>()).add(order);  // group into lists
config.getOrDefault("timeout", "30");                             // read with a fallback

The classic trap is using getOrDefault to group: map.getOrDefault(city, new ArrayList<>()).add(order) adds the order to a new list that is never stored in the map, so the map stays empty. getOrDefault only reads; computeIfAbsent inserts.

A few other Java 8 changes come up in interviews:

  • Metaspace replaced PermGen. Class information moved out of a fixed-size memory area into native memory, so OutOfMemoryError: PermGen space went away. Metaspace grows without limit unless you set -XX:MaxMetaspaceSize, so leaks (for example from redeploying an app without releasing its classes) now show up as Metaspace growth.
  • String.join(", ", parts) and java.util.Base64, small but handy.
  • HashMap tree bins: a heavily collided bucket becomes a tree, so lookups degrade to O(log n) instead of O(n) (covered in the Collections skill).

Common mistakes

MistakeWhat happensDo this instead
orElse(expensiveCall())the call runs every timeorElseGet(() -> expensiveCall())
isPresent() then get()a null check in disguisemap, ifPresent, orElseThrow
Optional.of(maybeNull)NullPointerExceptionOptional.ofNullable
Optional fields, parameters, Optional<List<T>>clumsy, and not Serializablenull inside the class; an empty list
Ignoring the result of plusDaysthe date doesn't changedate = date.plusDays(1)
LocalDateTime for timestampsambiguous across zonesInstant
Duration.ofDays(1) for "same time tomorrow"off by an hour across DSTplusDays(1)
A shared SimpleDateFormatwrong dates under loada DateTimeFormatter constant
getOrDefault(k, new List).add(x)the map stays emptycomputeIfAbsent(k, ...).add(x)

Check yourself

Try to answer before opening each one. Recalling beats re-reading.

1. Why does findUser(id).orElse(createGuest()) create a guest even when the user exists?

Show answer

orElse takes a value, so createGuest() runs before orElse is even called, every time. orElseGet(this::createGuest) only calls it when the Optional is empty.

2. Which type should a database column for "when the order was placed" map to, and why?

Show answer

Instant: it's one exact moment, the same in every zone, so it compares and sorts correctly. Convert to the user's zone only when showing it.

3. From noon on the day before clocks spring forward, what do plus(Duration.ofDays(1)) and plusDays(1) give?

Show answer

Duration.ofDays(1) adds exactly 24 hours and lands at 13:00 local time. plusDays(1) keeps the wall-clock time and lands at 12:00, only 23 hours later.

4. Why does map.getOrDefault(k, new ArrayList<>()).add(x) leave the map unchanged?

Show answer

getOrDefault returns the default without putting it in the map, so x goes into a list nobody keeps. computeIfAbsent(k, key -> new ArrayList<>()).add(x) inserts the list first.

Key takeaways

  • Optional is for return values: orElseGet for anything costly, map/orElseThrow instead of get, flatMap for nested Optionals.
  • java.time is immutable; pick Instant, LocalDate or ZonedDateTime by what the value means.
  • Duration counts exact time, Period and plusDays count calendar time; they differ across DST.
  • Share DateTimeFormatter, never SimpleDateFormat.
  • computeIfAbsent and merge replace check-then-act on maps; getOrDefault never inserts. Metaspace replaced PermGen, and needs a cap.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Optional

Optional.ofNullable(maybeNull);                  // not Optional.of (throws on null)
opt.map(User::name).orElse("unknown");           // orElse: argument evaluated EVERY time
opt.orElseGet(this::createGuest);                // lazy: only when empty
opt.orElseThrow(() -> new NotFound(id));
opt.flatMap(Customer::address);                  // when the function returns an Optional
opt.or(() -> fromDb(id));                        // Java 9: a second source

Return types only: not fields (not Serializable), not parameters, never Optional<List<T>>.

java.time types

ValueType
A moment ("order placed")Instant
A date ("due date", "birthday")LocalDate
A local time in a placeZonedDateTime (zone id, e.g. America/New_York)
An exact length ("30 s timeout")Duration
A calendar length ("1 month")Period

Immutable: d = d.plusDays(1). Format with a shared DateTimeFormatter, never a shared SimpleDateFormat.

DST in two lines

  • Spring forward: plus(Duration.ofDays(1)) from noon → 13:00; plusDays(1) → 12:00 (23 hours). Times in the gap move forward.
  • Fall back: 01:30 happens twice; atZone picks the earlier offset; withLaterOffsetAtOverlap() for the second.

Map methods

map.merge(k, 1, Integer::sum);
map.computeIfAbsent(k, key -> new ArrayList<>()).add(v);   // NOT getOrDefault(...).add(v)
map.getOrDefault(k, fallback);                             // reads only, never inserts

The traps

  • orElse(expensiveCall()); isPresent() + get().
  • Ignoring the return value of plusDays.
  • LocalDateTime for timestamps.
  • Duration vs plusDays across DST.
  • getOrDefault(...).add(...) leaves the map empty.
  • Metaspace is unbounded without -XX:MaxMetaspaceSize.

Staff-level points, one line each

  • Optional allocates, isn't Serializable (by design), and has OptionalInt/Long/Double for primitives; Java 9-11 added or, stream, ifPresentOrElse, orElseThrow(), isEmpty.
  • Store past events as Instant; future local events as local date-time + zone id (rules change, tzdata updates); never just an offset.
  • plusMonths clamps and drifts: compute from the original date (start.plusMonths(n)).
  • Period.between is calendar terms (P1M1D); ChronoUnit.DAYS.between counts days.
  • OffsetDateTime keeps its offset through arithmetic; ZonedDateTime re-applies the zone's rules.
  • Inject a Clock; every now(clock) takes one.
  • JDBC 4.2: DATE ↔ LocalDate, TIMESTAMP ↔ LocalDateTime, timestamptz ↔ OffsetDateTime/instant.

Say it in an interview

  • "orElse evaluates its argument eagerly, so for anything with a cost I'd use orElseGet."
  • "I'd store the order time as an Instant and convert to the user's zone only for display."
  • "Renewals are computed from the start date with plusMonths(n), so they don't drift after February."

Practice

This skill comes with 14 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Java 8: Optional, java.time and the Rest free

Subscription Renewal Times

MediumProCoding problemJava

A streaming service bills subscribers monthly, at the same local time they signed up, in their own time zone. A customer who signed up in New York on January 31 at 9:00 a.m. renews on February 28 (February has no 31st), March 31 and April 30, each at 9:00 a.m.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Java 8: Optional, java.time and the Rest free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime