Optional and Pattern Summary
java.util.Optional<T> is a container object that either holds a non-null value or is empty — an explicit, type-checked alternative to using null to mean “no value”.
The problems with null
Error-prone: It is easy to forget a null-check, leading to NullPointerException at runtime. The compiler does not help — any reference can be null at any time.
Verbose: Defensive code is littered with null-checks:
if (x != null) {
if (x.getY() != null) {
return x.getY().getZ();
}
}
return null;
No semantic meaning: A null String could mean “absent”, “not yet loaded”, “an error occurred”, or “the developer forgot to initialise it”. The null itself does not tell you why it is null.
How Optional solves these
Optional forces the caller to actively acknowledge the possibility of absence and handle it:
Optional<String> maybeName = findName(id);
// Old style (check-then-act — still possible, but the Optional signals absence)
if (maybeName.isPresent()) {
System.out.println(maybeName.get());
}
// Functional style (preferred — declarative, no if-checks)
maybeName.ifPresent(name -> System.out.println(name));
// Provide a default
String name = maybeName.orElse("Unknown");
// Lazy default (Supplier — only evaluated if needed)
String name = maybeName.orElseGet(() -> computeFallbackName());
// Throw if absent
String name = maybeName.orElseThrow(() -> new NotFoundException("No name for " + id));
Optional’s functional API
// Transform the value if present, producing a new Optional
Optional<String> upper = maybeName.map(String::toUpperCase);
// Transform and flatten (when the mapping function returns Optional)
Optional<Integer> length = maybeName.flatMap(n -> Optional.of(n.length()));
// Filter by predicate — empty if present but does not match
Optional<String> longName = maybeName.filter(n -> n.length() > 5);
When to use Optional (and when NOT to)
Designed for: Method return values that may legitimately be absent. For example, finding an entity by ID in a database — the entity might not exist.
NOT intended for:
| Misuse | Why it is wrong |
|---|---|
| Fields | Optional is not Serializable; adds boxing overhead; the absence signal should be in the method return |
| Method parameters | Forces callers to wrap values; awkward at call sites |
| Collection contents | An empty collection already means “no elements”; do not wrap List<Optional<T>> |
| Replacing all nulls | Null still has performance advantages for internal code; Optional is for public API boundaries |
Optional was introduced primarily to make API contracts explicit — when a method returns Optional<T>, the caller knows the value may be absent and the type system enforces handling that case.
Pattern summary table
| Pattern | Intent | Key structure | Principle |
|---|---|---|---|
| Composite | Treat one object and a group of objects uniformly | Interface → Leaf & Composite both implement it; Composite holds List<Interface> | OCP |
| Decorator | Add behaviour to an individual object dynamically | Interface → Base impl & Wrapper both implement it; Wrapper holds one Interface (inner) | OCP |
| State | Object changes behaviour when its internal state changes | Context holds State reference; state transitions from within the object | SRP, OCP |
| Strategy | Client swaps algorithm externally at runtime | Context holds Strategy reference; strategy chosen by client | OCP |
| Singleton | Exactly one instance with global access | Private constructor + static factory method | (Often criticised) |
| Observer | Notify many dependents automatically on state change | Subject holds List<Observer>; Observer interface with update() method | Loose coupling, OCP |
How to tell patterns apart in a Tripos scenario
- “Treat one and group uniformly” → Composite.
- “Add behaviour to one object, wrap/decorate it” → Decorator.
- “Behaviour changes based on internal state / lifecycle” → State.
- “Choose between interchangeable algorithms / ways of doing something” → Strategy.
- “Exactly one instance of this must exist” → Singleton.
- “Notify many things when something changes” → Observer.
Critical Tripos answer structure
For any pattern question:
- State the problem the scenario faces (what would go wrong without the pattern).
- Name the pattern explicitly.
- Sketch the participants — which interfaces, which concrete classes, what relationships exist between them.
- Write the code — adapt the pattern to the scenario’s specific domain, using the scenario’s actual class and method names.
- Justify the choice — tie it to OCP, loose coupling, or SRP. Explain what would have to change if a new requirement arrived and why the pattern makes that change trivial.
Never just describe a pattern in abstract terms — always ground it in the specific classes and methods from the question.