Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
- 21File I/O and Serialization
- 22Debugging: Errors, Stack Traces, the Debugger and Logging
- 23Maven: What It Does and How to Use It
- 24Git for Java Projects: Commits, Branches, Merging and Pull Requests
- 25Testing with JUnit and Mockito: Unit Tests, Mocks and Their Limits
- 26Project 1: Build an Expense Tracker CLI in Plain Java
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
- 36Security Fundamentals and Spring Security: Passwords, JWT, Access Control, CORS and Secrets
- 37Testing Spring Boot Applications: Slice Tests, MockMvc, Real Databases and the Test Pyramid
- 38Production Practices: Configuration, Logging, Docker and Health Checks for Spring Boot
- 39Project 3: The Job Application Tracker API, Assembled and Reviewed
Lesson 17 · Functional Java
Null and Optional
Why null causes so many bugs, what Optional is actually for, how to use map, flatMap, orElse, orElseGet and orElseThrow, and the common misuses (fields, parameters, get without a check). Optional is a tool for return values, not a universal replacement for null.
For
Learners who have met NullPointerException and want a principled approach to absent values, including the correct use of Optional.
Level
Before you start
Lambdas, Functional Interfaces and Method References (article 15) and Streams (article 16).
On this page
Key takeaways
- null means different things (absent, unknown, not yet set, error), and the type system does not tell you where it may appear. That is why NullPointerException is so common.
- Optional is a container that makes the possible absence of a return value visible in the type. Its Javadoc describes it as intended primarily for method return types where there is a clear need to represent no result.
- Prefer map, flatMap, filter, orElse, orElseGet, orElseThrow and ifPresent over isPresent followed by get. Use orElseGet when the fallback is expensive or has side effects, because orElse always evaluates its argument.
- Do not use Optional for fields, method parameters, collection elements or as a wrapper for everything. Return an empty collection instead of null or Optional for collections.
- Validate required arguments early with Objects.requireNonNull, and model absence deliberately instead of passing null around.
You call user.getAddress().getCity().toUpperCase() and the program crashes with a NullPointerException. Which of the three calls returned null? The message may tell you, and the code does not. Nothing in the type of getAddress() warned that the result might be missing.
This is the most common runtime error in Java, and it comes from a design choice: any reference variable can be null, and the compiler does not track where. Tony Hoare, who introduced null references into a language in 1965, later called it his "billion-dollar mistake". This article explains why null is a problem, what to do about it in general, and how Optional helps in one specific, useful role.
What you will learn
#- Why
nullis ambiguous and hard to reason about. - General habits that reduce null problems with or without
Optional. - What
Optionalis, and what it is designed for. - The operations that make
Optionaluseful:map,flatMap,filterand theor...family. - The common misuses, and when not to use it.
The problem with null
#null is a reference that points to nothing. Calling a method on it throws NullPointerException (NPE). The deeper problem is meaning. When a method returns null, does that mean "not found", "not applicable", "an error occurred", "not yet loaded", or "the programmer forgot to set it"? Different callers read it differently, so every caller must remember to check, and one missing check is a crash.
// File: Main.javapublic class Main { static class Address { private final String city; Address(String city) { this.city = city; } String city() { return city; } } static class User { private final String name; private final Address address; // may be null User(String name, Address address) { this.name = name; this.address = address; } Address address() { return address; } } static String cityOf(User user) { return user.address().city().toUpperCase(); } static String cityOfSafely(User user) { // the defensive version: a pyramid of checks if (user != null) { Address address = user.address(); if (address != null) { String city = address.city(); if (city != null) { return city.toUpperCase(); } } } return "UNKNOWN"; } public static void main(String[] args) { User asha = new User("Asha", new Address("Pune")); User ravi = new User("Ravi", null); System.out.println(cityOf(asha)); System.out.println(cityOfSafely(ravi)); System.out.println(cityOf(ravi)); }}PUNEUNKNOWNException in thread "main" java.lang.NullPointerException: Cannot invoke "Main$Address.city()" because the return value of "Main$User.address()" is nullThe first call works. The defensive version works but is a pyramid of checks that obscures the actual logic. The last call crashes. Since Java 14, the NPE message says exactly what was null (here, the result of the address() call). Helpful, but it still happens at runtime, in production, after the fact.
Habits that help, with or without Optional
#Before reaching for a new class, apply the basics:
- Do not return
nullfor collections. Return an empty list, set or map. Callers can then loop without a check. - Fail fast on required arguments.
Objects.requireNonNull(value, "value must not be null")at the top of a method or constructor turns a distant crash into an immediate, explained one. - Initialise fields. A field that is never
null(an empty list, an empty string, a default) removes a whole class of checks. - Make "absence" a deliberate part of the design. Decide for each value whether it can legitimately be missing, and document and model that.
- Prefer values that cannot be null (records with validated constructors, enums, sealed types).
- Do not use
nullto signal errors. Throw an exception, or return a result type (article 10).
A third-party approach is to annotate parameters and returns with @Nullable or @NonNull and let tools check them. Those annotations are not part of the standard library, and they are used by many frameworks, including Spring, but they are conventions and not a language feature.
Optional: making absence visible
#Optional<T> (Java 8) is a container that either holds one non-null value or is empty. The Javadoc describes it as a container object which may or may not contain a non-null value, and gives its purpose precisely: it is primarily intended for use as a method return type where there is a clear need to represent "no result", and where using null is likely to cause errors. A variable of type Optional should never itself be null.
Return type Optional<User> tells every caller, in the signature, "this may find nothing". The compiler cannot force you to handle the empty case, but the API makes it hard to forget.
Creating an Optional
#Factory | Use |
|---|---|
| The value is definitely non-null (throws |
| The value may be |
| No value |
Using an Optional: the operations
#The aim is to avoid unwrapping manually. These methods let you describe what to do with the value, whether or not it is there.
// File: Main.javaimport java.util.List;import java.util.NoSuchElementException;import java.util.Optional;public class Main { record Address(String city) {} record User(String name, Address address, String email) { Optional<Address> findAddress() { return Optional.ofNullable(address); } Optional<String> findEmail() { return Optional.ofNullable(email); } } static Optional<User> findUser(List<User> users, String name) { return users.stream().filter(u -> u.name().equals(name)).findFirst(); } public static void main(String[] args) { List<User> users = List.of( new User("Asha", new Address("Pune"), "asha@example.com"), new User("Ravi", null, null)); // map: transform the value if present; flatMap: when the function itself returns an Optional String city = findUser(users, "Asha") .flatMap(User::findAddress) .map(Address::city) .map(String::toUpperCase) .orElse("UNKNOWN"); String missing = findUser(users, "Ravi") .flatMap(User::findAddress) .map(Address::city) .orElse("UNKNOWN"); String nobody = findUser(users, "Zed") .flatMap(User::findAddress) .map(Address::city) .orElse("UNKNOWN"); System.out.println(city + " " + missing + " " + nobody); // filter: keep the value only if it satisfies a condition System.out.println(findUser(users, "Asha").flatMap(User::findEmail).filter(e -> e.endsWith("@example.com")).isPresent()); // ifPresent / ifPresentOrElse: perform an action findUser(users, "Ravi").flatMap(User::findEmail).ifPresentOrElse( e -> System.out.println("email: " + e), () -> System.out.println("no email on file")); // orElseThrow: absence is an error here try { findUser(users, "Zed").orElseThrow(() -> new IllegalArgumentException("no such user: Zed")); } catch (IllegalArgumentException e) { System.out.println(e.getMessage()); } try { findUser(users, "Zed").orElseThrow(); // no-argument form: NoSuchElementException } catch (NoSuchElementException e) { System.out.println("NoSuchElementException: " + e.getMessage()); } // or: an alternative Optional; stream: zero or one element in a stream System.out.println(findUser(users, "Zed").or(() -> findUser(users, "Ravi")).map(User::name).orElse("?")); System.out.println(users.stream().map(User::findEmail).flatMap(Optional::stream).toList()); }}PUNE UNKNOWN UNKNOWNtrueno email on fileno such user: ZedNoSuchElementException: No value presentRavi[asha@example.com]Compare this with the pyramid of null checks. The chain flatMap(...).map(...).map(...).orElse("UNKNOWN") describes the happy path once, and the empty case flows through to the end without any if. The important distinction between the two transforming methods:
map(f)appliesfto the value and wraps the result. Iffreturnsnull, you get an emptyOptional.flatMap(f)is for functions that already return anOptional. It avoids ending up with anOptional<Optional<T>>.
The terminal operations, from safest to least safe:
Method | Behaviour |
|---|---|
| The value, or the fallback |
| The value, or the supplier's result (computed only if needed) |
| The value, or throws. The no-argument form (Java 10) throws |
| Perform an action |
| Test (use sparingly) |
| The value, or throws if empty. Avoid, because it is a hidden check that people forget |
orElse versus orElseGet
#This difference is a classic interview question and a real source of bugs. orElse(x) takes a value, so the expression for x is always evaluated, even when the Optional has a value. orElseGet(supplier) takes a function and only calls it when needed.
// File: Main.javaimport java.util.Optional;public class Main { static int expensiveCalls = 0; static String loadDefault() { expensiveCalls++; return "default"; } public static void main(String[] args) { Optional<String> present = Optional.of("value"); String a = present.orElse(loadDefault()); // loadDefault() runs even though not needed System.out.println(a + " calls=" + expensiveCalls); String b = present.orElseGet(() -> loadDefault()); // not called: the value is present System.out.println(b + " calls=" + expensiveCalls); String c = Optional.<String>empty().orElseGet(() -> loadDefault()); System.out.println(c + " calls=" + expensiveCalls); }}value calls=1value calls=1default calls=2Use orElse for a cheap constant (orElse("UNKNOWN")) and orElseGet whenever computing the fallback costs something or has a side effect (a database call, a log entry, creating an object).
Optional in streams
#You have already seen Optional from stream terminal operations: findFirst, findAny, min and max return one. Optional::stream (Java 9) turns an Optional into a stream of zero or one elements, which lets flatMap(Optional::stream) skip the empty ones, as in the last line of the example. Numeric streams have their own OptionalInt, OptionalLong and OptionalDouble, such as the result of average().
Common misuses
#Optional is often overused, which creates noise without benefit. Avoid these:
Misuse | Why it is a problem | Instead |
|---|---|---|
A field of type |
| A nullable field, with an accessor that returns |
A method parameter of type | Callers must wrap values, and the parameter itself could still be | Overloads, or a plain parameter that may be |
| That is a null check in disguise, and hides nothing |
|
Calling | Throws |
|
| Throws an NPE immediately |
|
A method returning | Defeats the whole point | Return |
| An empty collection already means "nothing" | Return an empty collection |
Wrapping every return value | Noise when absence is not possible | Use |
Comparing with | The documentation says there is no guarantee that it is a singleton |
|
Using it for synchronisation or identity | It is a value-based class, and the documentation warns against it | Do not |
Chaining dozens of operations | Unreadable | Extract methods |
Here is a short before and after:
// File: Main.javaimport java.util.List;import java.util.Optional;public class Main { record Product(String name, Double discount) {} // Misuse: isPresent + get, and the fallback is computed eagerly static double finalPriceBad(Product p, double price) { Optional<Double> discount = Optional.ofNullable(p.discount()); if (discount.isPresent()) { return price - discount.get(); } return price; } // Better: describe the transformation static double finalPrice(Product p, double price) { return Optional.ofNullable(p.discount()).map(d -> price - d).orElse(price); } // Collections: never null, never Optional static List<String> tagsFor(String productName) { return productName.equals("pen") ? List.of("stationery", "office") : List.of(); } public static void main(String[] args) { Product pen = new Product("pen", 2.0); Product book = new Product("book", null); System.out.println(finalPriceBad(pen, 10) + " " + finalPriceBad(book, 10)); System.out.println(finalPrice(pen, 10) + " " + finalPrice(book, 10)); for (String tag : tagsFor("unknown product")) { // safe: an empty list simply loops zero times System.out.println(tag); } System.out.println(tagsFor("pen")); }}8.0 10.08.0 10.0[stationery, office]Optional is not a universal replacement for null
#It is tempting to conclude that null is banned and Optional is the fix for everything. The Javadoc says otherwise: it is for return values with a clear need to represent "no result". A balanced approach:
- Return values that may be absent:
Optional<T>. This is the main use (a lookup by id, a search, a first match). - Fields and parameters: allow
nullinternally where you must, validate withObjects.requireNonNullwhere you must not, and keep the nullability out of your public API where you can. - Collections and arrays: empty, never
null. - Alternatives that fit better: a default value, an exception for "must exist", a sealed result type (a record
Foundand a recordNotFound, as in article 9) when the reason for absence matters. - Boundaries: values arriving from a database, JSON or a library can be
null. Convert them at the boundary (Optional.ofNullable, or a default) so that the interior of your code deals with clean values.
In the Job Application Tracker project, repository methods such as findById return Optional<JobApplication>, and the service turns an empty result into a NotFound exception in one place (article 33).
Common mistakes
#- Using
Optionalfor fields or parameters. - Calling
get()without a check. - Writing
isPresent()thenget()wheremaporifPresentis clearer. - Using
orElsewith an expensive or side-effecting call. - Returning
nullfrom a method whose type isOptional. - Using
Optional.ofwith a value that can benull. - Wrapping collections in
Optional. - Treating
Optionalas a substitute for validation.
Exercises
#Exercise 1
Fix the chain
#Rewrite String zip = user.getAddress().getZipCode().trim(); using Optional, assuming getAddress() and getZipCode() may return null, producing "none" when anything is missing.
Exercise 2: orElse or orElseGet?
#Which would you use, and why: (a) name.orElse("anonymous"), (b) fallback is repository.loadDefaultFromDatabase(), (c) fallback is new ArrayList<>()?
Exercise 3: Find the misuse
#Identify the problems in this signature: public void register(Optional<String> email, Optional<List<String>> tags), and propose a better design.
Exercise 4: map or flatMap?
#Optional<User> user and Optional<Address> findAddress(User u). Write the expression that gets the user's address, and explain why map would give the wrong type.
Exercise 5: Design a return type
#You write findByEmail(String email) on a repository. Choose a return type among User, User or null, Optional<User> and throwing an exception, and justify. Then say what changes for getById(long id) on an entity that must exist.
Show solutionHide solution
String zip = Optional.ofNullable(user) .map(User::getAddress) .map(Address::getZipCode) .map(String::trim) .orElse("none");Each map returns an empty Optional if the function returns null, so a missing address or zip code short-circuits through the rest of the chain to "none". If getAddress() itself returned an Optional<Address>, you would use flatMap for that step.
Exercise 2
Show solutionHide solution
(a) orElse: a constant is cheap and has no side effect. (b) orElseGet(repository::loadDefaultFromDatabase): it avoids a database call when the value is present, since orElse would run it every time. (c) orElseGet(ArrayList::new), because creating an object per call is a small cost that you should only pay when needed (and it avoids sharing one mutable instance).
Exercise 3
Show solutionHide solution
Both parameters are Optional, which should not be used for parameters: it forces callers to wrap values, the parameter itself could still be null, and Optional<List<String>> is doubly redundant because an empty list already means "no tags". A better design is register(String email, List<String> tags) with Objects.requireNonNull(tags) (callers pass an empty list when there are none), and an overload or documented null for an optional email: register(String email) and register(String email, List<String> tags). If a value is required, validate it instead of making it optional.
Exercise 4
Show solutionHide solution
Optional<Address> address = user.flatMap(this::findAddress); The map method would apply the function and wrap its result, so user.map(this::findAddress) has type Optional<Optional<Address>>, an Optional nested inside an Optional. flatMap expects a function that already returns an Optional, and flattens the result into a single Optional<Address>.
Exercise 5
Show solutionHide solution
For findByEmail, Optional<User> is the best fit: "no user with that email" is a normal outcome that callers must decide how to handle, and the signature says so. Returning null hides the possibility, and throwing an exception makes a normal outcome exceptional. For getById(long id) where the entity must exist (for example when following a foreign key that the database guarantees), returning the User and throwing a not-found exception when it is absent can be right, because an absent value is a bug or an inconsistent state. The decision is about whether absence is a normal result or an error.
Where to go next
#- Next: How the JVM Runs Your Code: Memory, GC and JIT begins the JVM and concurrency module.
- For
Optionalin repositories: Building a REST API with Spring Boot. - For the interview version: Interview Concepts, Part 1.
Sources & further reading
- Technology
- Software Engineering
- #Java
- #Java backend
- #Java basics
- #Java interview
- #JPA
- #learn Java
- #modern Java
- #Optional
- #Spring Boot
- #SQL
Connected lessons
References
Linked from 6 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Java Essentials for DSA
Guide · Technology
Heaps and Priority Queues
Course · Technology
DSA From Zero to Job-Ready: A Practical Guide with Java

