Back to Optional
Q6Question 6 of 9

When NOT to use Optional?

Key Concepts
  • Never return Optional<List<T>>: an empty list already represents 'no results.' Return the empty list directly.
  • Use OptionalInt, OptionalLong, OptionalDouble instead of Optional<Integer/Long/Double> to avoid autoboxing.
  • Avoid Optional in hot paths: each Optional.of() allocates a heap object, adding GC pressure in tight loops.
  • Return empty collections (List.of(), Set.of(), Collections.emptyList()) instead of wrapping in Optional.
  • IntStream.max() returns OptionalInt directly; wrapping in Optional<Integer> creates unnecessary boxing.
  • Optional is designed for API clarity at method boundaries, not for replacing every null check in computation.
  • In tight loops processing millions of elements, a plain null check is significantly faster than Optional wrapping.

Definition

Never Optional<Collection> (return empty collection), never Optional<Integer> (use OptionalInt), and avoid Optional in hot loops (heap allocation overhead).
  • Empty collection > Optional: an empty list already means 'no results.' Wrapping adds confusion.
  • OptionalInt/Long/Double: avoid autoboxing for primitive values. IntStream.max() returns OptionalInt.
  • Hot paths: each Optional allocates a heap object. In loops with millions of iterations, this adds GC pressure.
  • Rule: use Optional at service boundaries (return types). Use plain values inside tight algorithms.

When National Bank of India says 'here are your transactions this month,' an empty list means no transactions. Wrapping it in an envelope marked 'might be empty' (Optional) is redundant: the list IS the answer. Storing a single integer in a fancy gift box (Optional) when a plain envelope (OptionalInt) exists wastes wrapping material (boxing).

Understanding the Concepts

When should you NOT use Optional?

Rule 1: Return empty collection, not Optional

JAVA
package com.bank.service;

import java.util.List;
import java.util.Optional;

public class EmptyCollectionRule {
    record Transaction(String id) {}

    // WRONG: Optional<List<T>>
    static Optional<List<Transaction>> findBad(String accountId) {
        List<Transaction> txns = List.of(); // empty
        return txns.isEmpty() ? Optional.empty() : Optional.of(txns);
    }

    // CORRECT: return empty list directly
    static List<Transaction> findGood(String accountId) {
        return List.of(); // empty list = no results
    }

    public static void main(String[] args) {
        // Bad: caller must unwrap
        findBad("SAV2001").orElse(List.of()).forEach(System.out::println);

        // Good: caller just iterates
        findGood("SAV2001").forEach(System.out::println);
    }
}

Rule 2: Use primitive OptionalInt/Long/Double

JAVA
import java.util.OptionalInt;
import java.util.stream.IntStream;

// WRONG: Optional<Integer> with autoboxing
// Optional<Integer> max = stream.map(...).max(Integer::compareTo);

// CORRECT: OptionalInt without boxing
OptionalInt max = IntStream.of(85, 92, 78, 95, 88).max();
max.ifPresent(score -> System.out.println("Max score: " + score));

Rule 3: Avoid Optional in hot paths

JAVA
// WRONG: millions of Optional allocations
for (int i = 0; i < 10_000_000; i++) {
    Optional<String> opt = Optional.ofNullable(cache.get(i)); // heap allocation!
    String val = opt.orElse("default");
}

// CORRECT: plain null check in hot path
for (int i = 0; i < 10_000_000; i++) {
    String val = cache.get(i);
    if (val == null) val = "default"; // zero allocation
}

Why does Optional allocate on the heap?

Every Optional.of(value) creates a new object on the heap (16 bytes header + reference). In tight loops processing millions of elements, this adds significant GC pressure. Plain null checks use zero allocation.

When is Optional appropriate?

| Context | Use Optional? | Why |
|---------|:---:|-----|
| Service method return type | Yes | Signals 'may be absent' |
| Repository method return type | Yes | findById may not find |
| Inside a tight loop | No | Heap allocation overhead |
| Collection result | No | Return empty collection |
| Primitive value | No | Use OptionalInt/Long/Double |
| Field storage | No | Not Serializable |
| Method parameter | No | Awkward API |

What about Optional with primitive wrappers?

JAVA
// WRONG: Optional<Integer> boxes twice (Integer wrapper + Optional wrapper)
Optional<Integer> opt = Optional.of(42); // Integer boxing + Optional allocation

// CORRECT: OptionalInt avoids both
OptionalInt opt = OptionalInt.of(42); // single primitive-aware allocation

// OptionalInt API:
optInt.isPresent();           // boolean
optInt.getAsInt();            // int (not get())
optInt.orElse(0);             // int default
optInt.ifPresent(i -> ...);   // IntConsumer

What are common mistakes?

JAVA
// WRONG: Optional<List<T>>
Optional<List<Account>> accounts = repository.findByBranch("Mumbai");
// Caller: accounts.orElse(List.of()).forEach(...) – awkward!

// CORRECT: return empty list
List<Account> accounts = repository.findByBranch("Mumbai");
// Caller: accounts.forEach(...) – clean!

// WRONG: Optional in a loop
for (Account a : accounts) {
    Optional.ofNullable(a.getEmail()).ifPresent(this::sendEmail);
}

// CORRECT: plain null check in a loop
for (Account a : accounts) {
    if (a.getEmail() != null) sendEmail(a.getEmail());
}

What should a senior engineer know?

  • Optional is designed for API clarity at method boundaries, not for replacing every null check in computation.
  • Use Optional at service boundaries (return types). Use plain values inside tight algorithms and performance-critical code.
  • Valhalla (Project Valhalla) may make Optional a value type in the future, eliminating heap allocation. Until then, avoid it in hot paths.
  • IntStream.max() returns OptionalInt directly; wrapping in Optional creates unnecessary boxing.

Visual Overview

DIAGRAM
  When NOT to use Optional:
  ─────────────────────────
  1. Optional<List<T>>  → return empty List directly
  2. Optional<Integer>  → use OptionalInt (no boxing)
  3. Hot loop Optional  → use null check (no allocation)

  Rule:
  ┌──────────────────────────────────────┐
  │ API boundary (return type) → Optional │
  │ Inside tight algorithm     → null    │
  │ Collection result          → empty() │
  │ Primitive value            → OptionalInt │
  └──────────────────────────────────────┘

Key Takeaway

Optional should not be used for collections (return empty list), primitive values (use OptionalInt), or hot paths (allocation overhead). It is designed for API clarity at method boundaries.

The rule: return empty collections directly, use primitive Optional variants, and reserve Optional for service-layer return types where absence needs explicit signalling.

Remember one sentence: return empty collections instead of Optional, use OptionalInt for primitives, and avoid Optional in tight loops.

---

Share
Use ← → arrow keys to navigate