On this page
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:
Optional: returning "maybe nothing" safely.- The one
Optionalmistake almost everyone makes. java.time: which type to use for which kind of time.- Daylight saving time, and why "a day" can mean 23 hours.
- The new
Mapmethods, 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)whenxmight benull;Optional.of(null)throws aNullPointerException.Optional.empty()is the empty box. - Avoid
get(). On an emptyOptionalit throwsNoSuchElementException: No value present, andif (opt.isPresent()) use(opt.get());is just a null check with extra steps. Usemap,ifPresent,orElseororElseThrow. flatMapfor functions that return anOptional. IfCustomer::addressreturnsOptional<Address>,mapwould give you anOptional<Optional<Address>>;flatMapflattens 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:
| Type | What it is | Use it for |
|---|---|---|
Instant | one exact moment, the same everywhere | timestamps: "order placed", log events. Store and compare these |
LocalDate | a date with no time or zone | birthdays, due dates, holidays |
LocalDateTime | a date and time with no zone | rarely right on a server: ambiguous until you add a zone |
ZonedDateTime | a date and time in a specific zone | showing times to users, scheduling at someone's local time |
Duration | an exact amount of time (seconds) | timeouts, "retry after 30 seconds" |
Period | an 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 spacewent 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)andjava.util.Base64, small but handy.HashMaptree 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
| Mistake | What happens | Do this instead |
|---|---|---|
orElse(expensiveCall()) | the call runs every time | orElseGet(() -> expensiveCall()) |
isPresent() then get() | a null check in disguise | map, ifPresent, orElseThrow |
Optional.of(maybeNull) | NullPointerException | Optional.ofNullable |
Optional fields, parameters, Optional<List<T>> | clumsy, and not Serializable | null inside the class; an empty list |
Ignoring the result of plusDays | the date doesn't change | date = date.plusDays(1) |
LocalDateTime for timestamps | ambiguous across zones | Instant |
Duration.ofDays(1) for "same time tomorrow" | off by an hour across DST | plusDays(1) |
A shared SimpleDateFormat | wrong dates under load | a DateTimeFormatter constant |
getOrDefault(k, new List).add(x) | the map stays empty | computeIfAbsent(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
Optionalis for return values:orElseGetfor anything costly,map/orElseThrowinstead ofget,flatMapfor nested Optionals.java.timeis immutable; pickInstant,LocalDateorZonedDateTimeby what the value means.Durationcounts exact time,PeriodandplusDayscount calendar time; they differ across DST.- Share
DateTimeFormatter, neverSimpleDateFormat. computeIfAbsentandmergereplace check-then-act on maps;getOrDefaultnever inserts. Metaspace replaced PermGen, and needs a cap.