Optional

NullPointerException is the most common crash in Java. It happens because a method quietly returned null and a caller forgot to check. Optional makes “there might be no result” part of the return type, and gives you safe ways to provide defaults, transform values and fail with clear errors. You'll learn how to use it well, and where not to use it.

Module 14 · Modern Java⏱ 20 minIntermediate9 practice problems
1 The story

The crash that only happened to new students

Riya's canteen app greets students by name: findStudent(roll).getName(). It works for everyone already registered. On admission day, the app crashes for every new student, because findStudent returns null for them.

returns null findStudent() null .getName() → NullPointerException returns Optional findStudent() [ Asha ] [ empty ] .map(Student::name).orElse("Guest") myphatshala.in
Riya

Sir, I can add if (s != null) everywhere. But how will I remember every place?

Arjun Sir

You won't. Nobody does. Make the method return Optional<Student>. Then the type itself tells every caller: “this might be empty, handle it”.

Riya

And handling it means more ifs?

Arjun Sir

No: map, orElse, orElseThrow. Let's look inside the box.

2 Definition

What is Optional?

Formal definition (write this in exams)

java.util.Optional<T> is a container object that may or may not contain a non-null value of type T. It is intended mainly as a method return type where there may be no result, to make the absence explicit and avoid returning null. An Optional is created with Optional.of(value) (value must be non-null), Optional.ofNullable(value) or Optional.empty(). Its value is accessed through methods such as orElse, orElseGet, orElseThrow and ifPresent, and transformed with map, filter and flatMap, which return Optionals. Calling get() on an empty Optional throws NoSuchElementException.

In simple words

Optional is a gift box that might be empty. You're not allowed to assume there's something inside; you say what to do in both cases.

Words you'll meet in this lesson

EmptyAn Optional with no value. Not the same as null.
PresentAn Optional holding a non-null value.
FallbackThe value to use when empty: orElse("Guest").
Lazy fallbackorElseGet(supplier) computes it only when needed.
Fail fastorElseThrow: stop with a clear error when a value is required.
flatMapChain a step that itself returns an Optional.

null vs Optional

Returning nullReturning Optional
Does the signature warn callers?NoYes: Optional<Student>
Forgetting to checkNullPointerException laterCode doesn't compile without unwrapping
Default values != null ? s.name : "Guest".map(Student::name).orElse("Guest")
Chaining lookupsNested null checksmap / flatMap chain
CostNoneOne small object per result

Exam tip: “Optional is a container that may or may not hold a non-null value, introduced in Java 8 to represent optional return values and reduce NullPointerExceptions. of throws on null; ofNullable doesn't. orElse always evaluates its argument; orElseGet evaluates only when empty.”

3 Types and ways to use it

The Optional toolkit

// create
Optional.of(value)          // value must not be null
Optional.ofNullable(maybe)  // null becomes empty
Optional.empty()
// check and unwrap
opt.isPresent()   opt.isEmpty()
opt.orElse(fallback)        opt.orElseGet(() -> compute())
opt.orElseThrow()           opt.orElseThrow(() -> new XyzException("..."))
opt.ifPresent(v -> ...)     opt.ifPresentOrElse(v -> ..., () -> ...)
// transform (return Optionals)
opt.map(Function)   opt.filter(Predicate)   opt.flatMap(v -> anotherOptional)   opt.or(() -> otherOptional)

Create

of for definite values, ofNullable at boundaries (map lookups, old APIs), empty for “nothing”.

wrap

Transform

map, filter, flatMap work only when a value is present; empty passes through.

chain

Unwrap

orElse, orElseGet for defaults; orElseThrow when the value is required.

get the value

Act

ifPresent and ifPresentOrElse run code without unwrapping.

side effects

Why not just call get()?

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Optional<String> coupon = Optional.empty();
        System.out.println("isPresent: " + coupon.isPresent());
        try {
            System.out.println(coupon.get());
        } catch (NoSuchElementException e) {
            System.out.println("get() on empty: " + e.getMessage());
        }
        System.out.println("safe: " + coupon.orElse("no coupon"));
    }
}
isPresent: false get() on empty: No value present safe: no coupon

Use Optional for return values, not everywhere. Don't make fields, method parameters or collection elements Optional (List<Optional<X>>): it's not Serializable, adds noise, and an empty list is already a fine “nothing”. And never return null from a method whose type is Optional.

4 Step by step

What happens in find(id).map(f).orElse(d)

  1. The finder returns a box

    Optional<String> name = findName("98100");
    Inside: Optional.ofNullable(map.get(phone)): a box holding the name, or the shared empty box.
  2. map checks the box

    If a value is present, map calls your function and wraps the result (via ofNullable) in a new Optional. If empty, it returns empty without calling it.
  3. filter can empty it

    If you add .filter(test), a present value that fails the test becomes empty.
  4. orElse unwraps

    Present: return the value. Empty: return the fallback. Either way you get a plain value, never null (unless the fallback is null).
  5. Or fail clearly

    .orElseThrow(() -> new IllegalArgumentException("unknown id " + id))
    When there's no sensible default, throwing a meaningful exception at this point is much easier to debug than an NPE three methods later.
5 See it happen

null vs Optional: both views

Left: what the caller sees with null and with Optional. Right: the Optional objects on the heap and what each method does with a full or empty box.

Optional: both views 6 steps
What you see Customer c = findByPhone("98xxx"); // may be null c.getName() // NullPointerException! Optional<Customer> c = findByPhone("98xxx"); c.map(Customer::name).orElse("Guest") findByPhone("97xxx") // not found .map(Customer::name).orElse("Guest") result null caller forgot to check → app crashes Optional Customer Asha Optional (empty) nothing "Asha" "Guest" the type itself says: “there may be no result” map runs only if a value is inside map is skipped; orElse supplies the fallback nothing in the method's type warns you A lookup that may return null. Forget one check: crash. Return an Optional instead. Found: map then use the value. Not found: an empty Optional. Empty: the fallback is used. myphatshala.in
Behind the screen HEAP #1 Optional value → #2 #2 Customer "Asha" #3 Optional.EMPTY value = null one shared instance call #1.map(Customer::name) → new #4 Optional["Asha"] #3.map(Customer::name) → returns #3 (still empty) #3.orElse("Guest") → "Guest" #3.get() → NoSuchElementException Optional.of(customer) wraps a reference Optional.empty() returns the shared empty box Optional.of: a box around a reference. Optional.empty: one shared empty box. map on a full box: a new box. map on an empty box: still empty. orElse unwraps with a fallback. get() on empty throws. myphatshala.in
6 Real-world use

Optional in real systems

Data-access layers, configuration lookups and stream results are the most common places you'll meet Optional.

Watch it with an example: applying a coupon that may not exist

At checkout the app looks up the coupon code the user typed. It might be unknown, or expired. An Optional pipeline handles all cases in one chain.

Coupon lookup with Optional 5 steps
Animated example find(code).filter(Coupon::valid).map(Coupon::off).orElse(0) SAVE50 found valid ✓ ₹50 off OLD20 found expired ✗ ₹0 off HELLO not found — ₹0 off Three users type three codes… no null checks, no if-else, no crash One chain for every case. Valid coupon: ₹50 off. Expired: filter empties it. Unknown: empty from the start. All cases handled. myphatshala.in

1. Database lookups

Repository methods like findById(id) in Spring Data return Optional<Customer>: the record may not exist.

findById(42)→Optional<Customer>→orElseThrow(NotFound)

2. Configuration

Optional settings get defaults; required ones fail at startup with a clear message.

config.get("timeout")→orElse("30s")→value

3. Stream results

findFirst, min, max and reduce return Optional because a stream can be empty.

today's orders→max(byAmount)→Optional<Order>
₹50 off

4. Optional user input

Coupons, referral codes and nicknames may or may not be supplied; Optional chains turn them into discounts or display names.

coupon code→filter(valid).map(off)→orElse(0)

Simplified: APIs that return many results return an empty list, not an Optional list.

7 Run the code

Greeting customers by phone number

import java.util.*;

public class Main {
    static Map<String, String> customers = Map.of("98100", "Asha", "98200", "Ravi");

    static Optional<String> findName(String phone) {
        return Optional.ofNullable(customers.get(phone));
    }

    public static void main(String[] args) {
        for (String phone : List.of("98100", "99999", "98200")) {
            String greeting = findName(phone)
                    .map(name -> "Welcome back, " + name)
                    .orElse("Welcome! Please sign up");
            System.out.println(phone + " -> " + greeting);
        }
        findName("98200").ifPresent(n -> System.out.println("Sending offer to " + n));
        findName("99999").ifPresentOrElse(
                n -> System.out.println("Sending offer to " + n),
                () -> System.out.println("No customer for 99999"));
    }
}
98100 -> Welcome back, Asha 99999 -> Welcome! Please sign up 98200 -> Welcome back, Ravi Sending offer to Ravi No customer for 99999
  • Optional.ofNullable(customers.get(phone))Map lookups return null when the key is missing; wrap at the boundary.
  • .map(name -> "Welcome back, " + name)Runs only when the customer exists.
  • .orElse("Welcome! Please sign up")The answer for unknown numbers.
  • ifPresentOrElse(..., ...)One action for present, another for empty.
8 Line by line

A full box and an empty box

Recorded from the real JVM. Watch two Optional objects appear: one holding “Asha”, and the shared empty one. On the empty box, map's lambda never runs.

Worked examples: real problems, solved and traced in memory

Three common Optional patterns: a lookup with a default, a chain of optional steps, and failing loudly with orElseThrow when a value is required.

Example 1Telecom · lookup

Plan name for a mobile number

Problem statement

A telecom app shows the customer's plan for a mobile number. Not every number is registered. Return Optional<String> from the lookup and display “No active plan” when it's empty.

Input
numbers 98100 (registered), 97000 (not)
Expected output
98100: Unlimited 599
97000: No active plan

Approach

  1. planFor wraps the map lookup with Optional.ofNullable.
  2. The caller uses orElse for the default text.
  3. No null checks in the caller.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Logistics · delivery

Delivery city through optional steps

Problem statement

An order may have a customer, and a customer may have a saved address. Get the delivery city in upper case, or "PICKUP" if any step is missing, without a single null check.

Input
order 1 (customer with address in Pune), order 2 (customer without address)
Expected output
PUNE, PICKUP

Approach

  1. Model optional parts as Optional getters.
  2. Chain with flatMap (the step itself returns an Optional) and map (plain value).
  3. End with orElse("PICKUP").

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Payments · required data

Fail fast when a value is required

Problem statement

A refund needs the original payment. If the payment id isn't found, the refund must stop with a clear error, not continue with missing data. Use orElseThrow.

Input
refund for PAY-1 (exists), PAY-9 (missing)
Expected output
refunded Rs 640 for PAY-1
refund failed: unknown payment PAY-9

Approach

  1. findPayment returns Optional<Integer> (the amount).
  2. orElseThrow(() -> new IllegalArgumentException(...)) unwraps or throws.
  3. The caller catches the exception and reports it.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 14.4 in one screen

  • Optional = a box that holds one non-null value or nothing; use it as a return type.
  • Create: of (non-null), ofNullable, empty.
  • Unwrap: orElse, orElseGet (lazy), orElseThrow.
  • Act: ifPresent, ifPresentOrElse. Avoid bare get().
  • Transform: map, filter, flatMap; empty passes through untouched.
  • Streams' findFirst, min, max return Optional.
  • Not for fields, parameters or collections; never return null as an Optional.

Interview and viva questions

What problem does Optional solve?

It makes the possible absence of a result explicit in a method's return type, so callers are forced to handle the empty case, reducing NullPointerExceptions. It also provides a fluent API (map, filter, orElse) for working with possibly-missing values.

Difference between orElse and orElseGet?

orElse(x) takes an already-evaluated value, so the expression is computed even when the Optional has a value. orElseGet(supplier) calls the supplier only when the Optional is empty. Use orElseGet when the default is expensive or has side effects.

Difference between map and flatMap on Optional?

map applies a function returning a plain value and wraps it; flatMap applies a function that already returns an Optional, avoiding Optional<Optional<T>>. Use flatMap to chain lookups that each may be empty.

Should you use Optional as a field or parameter?

Generally no. Optional isn't Serializable, adds allocation, and as a parameter it forces callers to wrap values. Prefer overloading or nullable parameters documented clearly for inputs, and plain nullable fields with Optional-returning getters.

Is Optional.get() bad?

It throws NoSuchElementException on empty, so isPresent() + get() is just a disguised null check. Prefer orElse/orElseThrow/ifPresent/map. Java 10 added orElseThrow() as a clearer name for the same behaviour as get().

Practice problems

Create Optionals, unwrap them safely, and chain map, filter and flatMap. Every output is fixed. The last three are business tasks.

P1Easy

of, empty, ofNullable

Create an Optional with a value, an empty one, and one from a null reference, and print all three.

Input
(none)
Expected output
Optional[chai] Optional.empty Optional.empty
Show solution and explanation
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        String missing = null;
        System.out.println(Optional.of("chai") + " " + Optional.empty() + " " + Optional.ofNullable(missing));
    }
}
Optional[chai] Optional.empty Optional.empty

How it works: of requires a non-null value (it throws NPE on null); ofNullable accepts null and returns empty.

P2Easy

orElse

Print the nickname of a user if present, else their full name.

Input
nickname missing
Expected output
Hello, Meenakshi Iyer
Show solution and explanation
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> nickname = Optional.empty();
        String fullName = "Meenakshi Iyer";
        System.out.println("Hello, " + nickname.orElse(fullName));
    }
}
Hello, Meenakshi Iyer

How it works: orElse returns the value or the given fallback.

P3Easy

ifPresent

Print an SMS only if a phone number is present. Try with one present and one empty.

Input
98100 / none
Expected output
SMS sent to 98100
done
Show solution and explanation
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> p1 = Optional.of("98100");
        Optional<String> p2 = Optional.empty();
        p1.ifPresent(n -> System.out.println("SMS sent to " + n));
        p2.ifPresent(n -> System.out.println("SMS sent to " + n));
        System.out.println("done");
    }
}
SMS sent to 98100 done

How it works: the Consumer runs only when a value is present; nothing happens for empty.

P4Medium

orElse vs orElseGet

Show that orElse evaluates its argument even when a value is present, while orElseGet doesn't.

Input
(none)
Expected output
orElse:
  (loading default from server...)
dark
orElseGet:
dark
Show solution and explanation
import java.util.Optional;

public class Main {
    static String loadDefault() {
        System.out.println("  (loading default from server...)");
        return "default";
    }

    public static void main(String[] args) {
        Optional<String> theme = Optional.of("dark");
        System.out.println("orElse:");
        System.out.println(theme.orElse(loadDefault()));
        System.out.println("orElseGet:");
        System.out.println(theme.orElseGet(Main::loadDefault));
    }
}
orElse: (loading default from server...) dark orElseGet: dark

How it works: orElse(x) receives an already-computed value, so loadDefault() runs first anyway. orElseGet takes a Supplier and calls it only when empty: use it when the fallback is expensive.

P5Medium

map and filter on Optional

From an optional email, keep it only if it ends with "@company.in", and return the part before @, else "external".

Input
asha@company.in / ravi@gmail.com
Expected output
asha, external
Show solution and explanation
import java.util.Optional;

public class Main {
    static String userId(Optional<String> email) {
        return email.filter(e -> e.endsWith("@company.in"))
                    .map(e -> e.substring(0, e.indexOf('@')))
                    .orElse("external");
    }

    public static void main(String[] args) {
        System.out.println(userId(Optional.of("asha@company.in")) + ", " + userId(Optional.of("ravi@gmail.com")));
    }
}
asha, external

How it works: filter turns a full box empty if the test fails; map transforms only full boxes.

P6Medium

Optional from a stream

Find the first order above ₹5,000; print it or "none". Then find the cheapest order.

Input
450, 1200, 2600
Expected output
big: none, cheapest: 450
Show solution and explanation
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<Integer> orders = List.of(450, 1200, 2600);
        Optional<Integer> big = orders.stream().filter(o -> o > 5000).findFirst();
        Optional<Integer> cheapest = orders.stream().min(Integer::compare);
        System.out.println("big: " + big.map(String::valueOf).orElse("none") + ", cheapest: " + cheapest.orElse(0));
    }
}
big: none, cheapest: 450

How it works: stream methods that might have no answer (findFirst, min, max, reduce without identity) return Optional.

P7Business problem

Loyalty points or welcome bonus

Look up loyalty points by customer id; new customers get a 50-point welcome bonus. Use Optional.ofNullable on a map lookup.

Input
ids C1 (320 points), C9 (new)
Expected output
C1: 320, C9: 50
Show solution and explanation
import java.util.*;

public class Main {
    static Map<String, Integer> points = Map.of("C1", 320, "C2", 75);

    static int pointsFor(String id) {
        return Optional.ofNullable(points.get(id)).orElse(50);
    }

    public static void main(String[] args) {
        System.out.println("C1: " + pointsFor("C1") + ", C9: " + pointsFor("C9"));
    }
}
C1: 320, C9: 50

How it works: wrapping a possibly-null lookup at the boundary keeps nulls from spreading. (For maps alone, getOrDefault does the same job.)

P8Business problem

Shipping charge rule chain

An order may have a pincode. If present and it starts with "4" (Maharashtra), shipping is ₹0; if present but elsewhere, ₹49; if missing, ask for it.

Input
pincodes 411001, 560001, none
Expected output
free shipping
Rs 49 shipping
please enter a pincode
Show solution and explanation
import java.util.*;

public class Main {
    static String shipping(Optional<String> pin) {
        return pin.map(p -> p.startsWith("4") ? "free shipping" : "Rs 49 shipping")
                  .orElse("please enter a pincode");
    }

    public static void main(String[] args) {
        System.out.println(shipping(Optional.of("411001")));
        System.out.println(shipping(Optional.of("560001")));
        System.out.println(shipping(Optional.empty()));
    }
}
free shipping Rs 49 shipping please enter a pincode

How it works: map handles the “present” logic, orElse the “missing” case. (Illustrative rule.)

P9Business problem

Required config value

An app reads the GST rate from config. If it's missing, startup must fail with a clear message. Show both cases.

Input
config with and without gst.rate
Expected output
rate: 0.18
startup stopped: gst.rate missing in config
Show solution and explanation
import java.util.*;

public class Main {
    static double gstRate(Map<String, String> config) {
        return Optional.ofNullable(config.get("gst.rate"))
                .map(Double::parseDouble)
                .orElseThrow(() -> new IllegalStateException("gst.rate missing in config"));
    }

    public static void main(String[] args) {
        System.out.println("rate: " + gstRate(Map.of("gst.rate", "0.18")));
        try {
            gstRate(Map.of());
        } catch (IllegalStateException e) {
            System.out.println("startup stopped: " + e.getMessage());
        }
    }
}
rate: 0.18 startup stopped: gst.rate missing in config

How it works: failing at startup with a precise message is far better than running with a wrong tax rate. orElseThrow makes “required” explicit.

Real-world scenario questions (asked in company interviews)

Optional questions test whether you can remove NPEs without making the code worse.

Production · NPEFresher

NullPointerException on the order-tracking page

The tracking page calls order.getRider().getPhone(). Orders that aren't assigned to a rider yet crash the page. How would you fix it so the page shows “Rider not assigned yet”?

The interviewer is checking: chaining optional relationships.

Show a strong answer
  • Make the getter honest: Optional<Rider> rider().
  • Page code: order.rider().map(Rider::phone).orElse("Rider not assigned yet").
  • Search for other callers of getRider(); the new type makes the compiler point them out.

Avoid: wrapping the whole page in try { … } catch (NullPointerException e).

Performance · defaultsJunior

Every request hits the pricing service

Code reads cachedPrice(id).orElse(pricingService.fetch(id)). Monitoring shows the pricing service is called on every request, even when the cache has the price. Why?

The interviewer is checking: orElse vs orElseGet.

Show a strong answer
  • orElse's argument is evaluated before the call, so fetch(id) always runs.
  • Use orElseGet(() -> pricingService.fetch(id)): the supplier runs only on a cache miss.

Avoid: adding an if-isPresent around it; orElseGet already expresses it.

Code review · misuseJunior

Optional everywhere

A pull request changes every entity field to Optional<...> and every method parameter too, “to remove nulls from the codebase”. Serialization of sessions now fails. What feedback would you give?

The interviewer is checking: knowing Optional's intended scope.

Show a strong answer
  • Optional is designed for return types. It's not Serializable, so fields break serialization and many frameworks.
  • Keep fields as plain (possibly null) references and expose Optional-returning getters for the optional ones.
  • For parameters, use overloads or validate non-null inputs with Objects.requireNonNull.

Avoid: approving because it “looks modern”.

API design · errorsMid

Empty Optional or exception?

A PaymentService.getPayment(id) is called from refunds (where the id must exist) and from a search box (where it may not). Should it return Optional<Payment> or throw when not found?

The interviewer is checking: designing for callers.

Show a strong answer
  • Return Optional<Payment> from the lookup: “not found” is a normal outcome for search.
  • Refunds call getPayment(id).orElseThrow(() -> new PaymentNotFound(id)), choosing their own exception and message.
  • One method serves both cases; each caller decides what absence means.

Avoid: returning null for search and throwing for refunds from two near-identical methods.