Why null is a problem
null means "no object here". The trouble is that the type does not warn you. A User variable might hold a user, or it might hold null.
User user = userRepository.findUserById("u42");
String city = user.getAddress().getCity(); // which of these is null?findUserById returns User. Does it return null when the user is missing, or throw? You cannot tell from the signature. You have to read the code or find out in production.
So people make one of two mistakes. They forget the check and get a NullPointerException (NPE). Java throws an NPE when you call a method or read a field on null. Or they get scared and add if (x != null) everywhere, which buries the real logic. NPEs are still among the most common exceptions in production Java logs. Almost every one comes from a value that someone assumed could never be missing.
Optional<T> (Java 8) is a box that either holds one non-null value or is empty. It puts "this may have no result" into the method signature. The return type now says: "there may be no answer, and the caller must decide what that means." It does not remove every possible null, but it makes this one boundary honest.
Optional<User> findUserById(String id); // the signature now tells the truthOptional basics
Creating one
Optional.of(user) // user must not be null, throws NPE if it is
Optional.ofNullable(user) // empty if user is null, otherwise holds it
Optional.empty() // explicitly emptyUse ofNullable when the value might be null, like map.get(key). Use of when it must never be null, so a mistake fails loudly right away.
public Optional<User> findUserById(String id) {
return Optional.ofNullable(users.get(id));
}Getting the value out
User user = findUserById("u42").orElse(guestUser);
User user = findUserById("u42").orElseGet(() -> createGuestUser());
User user = findUserById("u42").orElseThrow(); // NoSuchElementException (Java 10)
User user = findUserById("u42")
.orElseThrow(() -> new UserNotFoundException("u42"));Avoid get(). It throws if the Optional is empty, so it is a null pointer bug with a different name. if (opt.isPresent()) { opt.get() } is also just a null check written the long way. The no argument orElseThrow() does exactly what get() does. Java 10 added it because the name get hides the fact that it can throw.
orElse vs orElseGet
Interviewers like this one because the wrong choice has a hidden cost.
static User createGuestUser() {
System.out.println("creating guest user");
return new User("guest", "Guest");
}
User a = findUserById("u42").orElse(createGuestUser());
// prints "creating guest user" even though u42 exists
User b = findUserById("u42").orElseGet(() -> createGuestUser());
// prints nothing, the supplier is only called when the Optional is emptyorElse(x)is eager, so it always runs. Java works outxbeforeorElseis called, like any method argument.orElseGet(supplier)is lazy. The supplier runs only if the value is missing.
For a constant like "Unknown" or 0, orElse is fine. If the default hits the database, creates objects, or calls another service, use orElseGet.
map, flatMap, filter
These let you work on the value inside the Optional without taking it out first.
// map: change the value inside, if there is one
String name = findUserById("u42")
.map(User::getName)
.orElse("Guest");
// flatMap: when the function itself returns an Optional
String city = findUserById("u42")
.flatMap(User::getAddress) // getAddress() returns Optional<Address>
.map(Address::getCity)
.orElse("Unknown");
// filter: keep the value only if it passes a test
findUserById("u42")
.filter(User::isPremium)
.ifPresentOrElse(
u -> System.out.println("Free delivery for " + u.getName()),
() -> System.out.println("Delivery fee Rs 30"));Why flatMap there? getAddress() already returns an Optional. map would wrap it again and give Optional<Optional<Address>>. flatMap removes the extra layer, as it does on streams.
One handy detail: if the function in map returns null, you get an empty Optional, not a crash.
findUserById("u7").map(User::getEmail); // Optional.empty if u7 has no emailOnce the chain goes empty, every later step is skipped and you land on the fallback.
Where to use Optional, and where not
The Java designers meant Optional for one job: return types that might have no result.
Good:
Optional<User> findUserById(String id)
Optional<Order> findLatestOrder(String userId)
Optional<Player> getWinner()Not good:
| Where | Why not | Do this instead |
|---|---|---|
| Fields | Adds an object per field, and Optional is not Serializable (it cannot be turned into bytes to save or send) | A nullable field with a getter that returns Optional |
| Method parameters | Callers can still pass null for the Optional itself, and calls get ugly | Overloads, or two separate methods |
Collections (Optional<List<Order>>) | An empty list already means "nothing" | Return an empty list |
Inside collections (List<Optional<X>>) | Hard to read, almost always a design smell | Filter the empties out |
| DTOs and JSON or JPA entities | Most frameworks do not handle it well | Plain nullable fields |
A parameter example. This:
void reportGameResult(Player p1, Player p2, Optional<Player> winner)reads better as two methods:
void reportWin(Player winner, Player loser)
void reportDraw(Player p1, Player p2)In an interview, returning Optional from a finder is enough to show you know this. Real codebases often go one step further. Teams mark nullable fields and parameters with an annotation such as JSpecify's @Nullable. The IDE or a build time checker like NullAway then flags any use that skips the null check. This catches the NPE before the code ships, which Optional alone cannot do for fields and parameters.
Fail fast with Objects.requireNonNull
The worst NPE is one that happens far from where the null came in. A null restaurant id gets stored in the constructor, and three calls later something crashes inside a pricing method with no clue where it came from.
Check at the door instead:
public class Order {
private final String orderId;
private final String restaurantId;
private final PaymentMode paymentMode;
public Order(String orderId, String restaurantId, PaymentMode paymentMode) {
this.orderId = Objects.requireNonNull(orderId, "orderId must not be null");
this.restaurantId = Objects.requireNonNull(restaurantId, "restaurantId must not be null");
this.paymentMode = Objects.requireNonNullElse(paymentMode, PaymentMode.COD);
}
}requireNonNull(x, message)throws right away with your message ifxis null, and returnsxotherwise, so you can assign in one line.requireNonNullElse(x, fallback)(Java 9) gives a default, so a nullpaymentModeabove becomesCOD.requireNonNullElseGet(x, supplier)is the lazy version, likeorElseGet.
Now the stack trace points at the constructor call that passed the null.
Return empty collections, never null
// Bad
public List<Order> getOrders(String userId) {
if (!ordersByUser.containsKey(userId)) return null;
return ordersByUser.get(userId);
}
// Good
public List<Order> getOrders(String userId) {
return ordersByUser.getOrDefault(userId, List.of());
}Every caller can now loop, stream or call isEmpty() without a check. An empty list already says "nothing here", so there is no need for null or Optional.
One catch: List.of() cannot be changed. A caller who tries to add to it gets UnsupportedOperationException, which is usually what you want. But when the user has orders, the good version hands out the internal list itself, so callers could change it. To stop that, wrap it with List.copyOf or Collections.unmodifiableList (see 19-StringsAndImmutability.md).
Null Object pattern
Sometimes "nothing" has a sensible behaviour. A missing discount means a discount of zero. Instead of passing null and checking for it everywhere, make an object that does nothing.
public interface Discount {
BigDecimal apply(BigDecimal amount);
}
public class FlatDiscount implements Discount {
private final BigDecimal off;
public FlatDiscount(BigDecimal off) { this.off = off; }
public BigDecimal apply(BigDecimal amount) { return amount.subtract(off).max(BigDecimal.ZERO); }
}
public class NoDiscount implements Discount {
public BigDecimal apply(BigDecimal amount) { return amount; }
}FlatDiscount uses max(BigDecimal.ZERO) so a large discount never makes the price negative.
// Before: every caller checks
BigDecimal total = discount != null ? discount.apply(subtotal) : subtotal;
// After: no check needed
BigDecimal total = discount.apply(subtotal); // discount is NoDiscount when there is noneUse it when "nothing" has an obvious default behaviour. Do not use it when the caller really needs to know the value was missing. A NoUser object that quietly lets anonymous people check out is worse than an empty Optional.
Helpful NullPointerException messages (Java 14+)
Older Java gave you only a line number. With a chain like order.getUser().getAddress().getCity() on one line, you had to guess which part was null.
Since Java 14 (switched on by default from Java 15), the message tells you which part was null:
Cannot invoke "Address.getCity()" because the return value of "User.getAddress()" is null
For unboxing:
Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
Local variable names show up only if the code was compiled with debug info (javac -g). Maven and Gradle do that by default. Otherwise you see "<local4>" instead of the name.
Common mistakes
- Calling
opt.get()without checking. UseorElse,orElseGetororElseThrow. - Returning
nullfrom a method whose return type isOptional. The whole point is lost, and callers crash onnull.isPresent(). - Using
orElsewith an expensive default. It runs every time. UseorElseGet. Optional.of(x)whenxmight be null. UseofNullable.- Optional fields and parameters. Keep Optional for return types.
Optional<List<X>>. Return an empty list.- Checking for null deep inside the code instead of rejecting it at the constructor.
Interview questions
"What is the difference between orElse and orElseGet?"
orElsetakes a value, so its argument is always evaluated, even when the Optional has a value.orElseGettakes a supplier that runs only when the Optional is empty, so use it when the default is expensive.
"Where should you use Optional?"
As the return type of methods that may have no result, like
findUserById. Not for fields, method parameters or collections, and never returnnullfrom a method that returns Optional.
"What is the difference between map and flatMap on Optional?"
mapwraps the function's result in an Optional. If the function already returns an Optional,mapgives youOptional<Optional<T>>, andflatMapflattens that toOptional<T>.
"What does Objects.requireNonNull give you?"
It throws a NullPointerException with your message the moment a null comes in, usually in the constructor. The failure then points at the caller who passed the null instead of some later line that used it.
"Should a method that returns a list return null when there are no results?"
No. Return an empty list. Callers can loop over it safely without a null check, and empty already means "no results".
"What is the Null Object pattern?"
Instead of using null for "nothing", you pass an object that implements the same interface and does nothing, like a
NoDiscountthat returns the amount unchanged. Callers stop checking for null. Only use it when doing nothing is the right default.
Where you see it in this repo
- Optional as a finder return type:
TicketRepository.findById,PaymentRepository.findByIdand the other repositories inLLD/ParkingLot/src/repositoryreturnOptional.ofNullable(map.get(id)). That is the textbook use. orElseThrow:AdminServicein the same project doesfloorRepository.findByNumber(...).orElseThrow(...)and the same for pricing rules.isEmptythenget:PricingService.calculateFeechecksruleOpt.isEmpty()and later callsruleOpt.get(). It works, but it is the null check style written with Optional. Amap(...).orElse(50.0)chain would avoid the separateget().- Optional return:
Board.getWinner()inOOOD/07-tic-tac-toereturnsOptional<Player>, since a game can end with no winner. - Optional as a parameter:
ScoreTracker.reportGameResult(Player, Player, Optional<Player>)in the same game. This is the case the table above warns about. Two methods, one for a win and one for a draw, would read better. requireNonNullElseGet:Ticket.calculateParkingDuration()inOOOD/01-parking-lotusesObjects.requireNonNullElseGet(exitTime, LocalDateTime::now)so a ticket with no exit time yet is priced up to the current time.- Methods that still return null:
ParkingManager.findSpotForVehicle,ParkingManager.parkVehicleandParkingLot.enterVehicleinOOOD/01-parking-lotreturnnullwhen no spot is free, andZomatoApp.checkoutinLLD/Zomatoreturnsnullfor an empty cart. These are good candidates forOptionalor a clear exception. - A Null Object candidate:
Receipt.printReceipt()inOOOD/06-grocery-store-systemdoesgetAppliedDiscounts().get(item)and checksif (discount != null). ANoDiscountdefault would remove the null check from the price maths. The receipt still needs to know whether a discount applied, so it can decide whether to print the discount line.
Related: 16-LambdasAndStreams.md for the method references used with map, and 20-MemoryAndPassByValue.md for the NPE you get from unboxing a null.