Java Optional is a container object that either holds one non-null value or is empty, and we return it from a method to say that a result may be missing. The caller sees the possible absence in the method signature and has to decide what happens when no value is there, instead of finding out through a NullPointerException.
We use Optional as the return type of lookups that can find nothing, such as findById() in a repository, a config key that may not be set, or the first element that matches a filter. The class lives in java.util and has been part of the JDK since Java 8, with new methods in Java 9, 10 and 11.
The following example shows the methods we use most, each with its result.
Optional<String> title = Optional.of("Dune");
Optional<String> none = Optional.empty();
boolean present = title.isPresent(); // true
boolean empty = none.isEmpty(); // true
String upper = title.map(String::toUpperCase).orElse("?"); // "DUNE"
String shortOnly = title.filter(t -> t.length() < 4).orElse("long title"); // "long title"
String fallback = none.or(() -> Optional.of("Emma")).orElseThrow(); // "Emma"
String value = title.orElseThrow(); // "Dune"
long count = none.stream().count(); // 0
Notice that no line checks for null, and the empty Optional flows through map(), filter() and stream() without failing. We go through creating an Optional, reading its value safely, chaining transformations, combining fallbacks and streams, and the places where Optional does more harm than good.
1. What Is Optional in Java?
In Java, null is the only value of the special null type, which has no name, and the JLS lets us assign it to any reference type. So a method declared to return Book may return a book or null, and nothing in the signature tells the caller which one to expect. A caller who skips the Javadoc calls a method on the result and gets a NullPointerException.
An Optional<Book> return type makes the missing case part of the API. The value inside is either present or absent, and the Optional itself is never null. To get at the book, the caller has to pick a method such as orElse(), orElseThrow() or ifPresent(), and each of them states what happens for the empty case.

The class is primarily intended for method return types where there is a clear need to represent no result. It does not replace every null in a code base, and section 8 lists the places where it does not belong.
The following example is a small library catalog. A Book may have a borrower, and a Member may have no email address, so both records return an Optional from their lookup methods.
record Member(String name, String email) {
Optional<String> emailAddress() {
return Optional.ofNullable(email);
}
}
record Book(String title, Member borrower) {
Optional<Member> currentBorrower() {
return Optional.ofNullable(borrower);
}
}
class Catalog {
private final Map<String, Book> books = new HashMap<>();
void add(Book book) {
books.put(book.title(), book);
}
Optional<Book> findByTitle(String title) {
return Optional.ofNullable(books.get(title));
}
}
static Catalog sampleCatalog() {
Catalog catalog = new Catalog();
catalog.add(new Book("Dune", new Member("Lokesh", "lokesh@mail.com")));
catalog.add(new Book("Emma", new Member("Ana", null)));
catalog.add(new Book("Ulysses", null));
return catalog;
}
2. Creating an Optional
The Optional class has no public constructor. We create instances with three static factory methods, and the choice depends on whether the value can be null.
- Optional.empty() returns an empty Optional.
- Optional.of(value) wraps a value that must not be null, and throws NullPointerException right away for null.
- Optional.ofNullable(value) returns an empty Optional for null and a present one for anything else.
Optional<String> nothing = Optional.empty(); // Optional.empty
Optional<String> dune = Optional.of("Dune"); // Optional[Dune]
Optional<String> fromMap = Optional.ofNullable(null); // Optional.empty
Optional<String> broken = Optional.of(null); // NullPointerException
We use of() when a null would be a bug, because the exception points to the line that produced it. We use ofNullable() to wrap results of older APIs that return null, such as Map.get(), the way Catalog.findByTitle() does.
3. Checking Whether a Value Is Present
The isPresent() method returns true when a value is there, and isEmpty(), added in Java 11, returns the opposite. Both are fine for an if condition, but an isPresent() check followed by a get() call is a null check written with more characters.
Catalog catalog = sampleCatalog();
boolean hasDune = catalog.findByTitle("Dune").isPresent(); // true
boolean noHobbit = catalog.findByTitle("Hobbit").isEmpty(); // true
When we want to run code only for a present value, ifPresent() takes a Consumer. The ifPresentOrElse() method, added in Java 9, also takes a Runnable for the empty case. For example, a reminder job sends a message to the borrower of a book, or logs that the book is on the shelf.
Catalog catalog = sampleCatalog();
List<String> reminders = new ArrayList<>();
catalog.findByTitle("Dune")
.flatMap(Book::currentBorrower)
.ifPresent(member -> reminders.add("Remind " + member.name()));
catalog.findByTitle("Ulysses")
.flatMap(Book::currentBorrower)
.ifPresentOrElse(member -> reminders.add("Remind " + member.name()),
() -> reminders.add("Ulysses is on the shelf"));
List<String> sent = reminders; // [Remind Lokesh, Ulysses is on the shelf]
4. Getting the Value Out of an Optional
Every Optional ends with a method that turns it back into a plain value. The right method depends on what an empty result means for the caller.
| Method | Empty Optional | Use it when |
|---|---|---|
| orElse(value) | Returns the given value | The default is a constant or already computed |
| orElseGet(supplier) | Calls the supplier | The default needs a method call or has side effects |
| orElseThrow() | Throws NoSuchElementException | A missing value is a bug (Java 10) |
| orElseThrow(supplier) | Throws the exception we create | A missing value needs a specific exception |
| get() | Throws NoSuchElementException | Never in new code, orElseThrow() says the same thing more clearly |
Catalog catalog = sampleCatalog();
String found = catalog.findByTitle("Emma").map(Book::title).orElse("not in catalog"); // "Emma"
String missing = catalog.findByTitle("Hobbit").map(Book::title).orElse("not in catalog"); // "not in catalog"
Book dune = catalog.findByTitle("Dune").orElseThrow(); // Book[title=Dune, borrower=Member[name=Lokesh, email=lokesh@mail.com]]
Book hobbit = catalog.findByTitle("Hobbit").orElseThrow(); // NoSuchElementException: No value present
The orElse() method always evaluates its argument, even for a present value, whereas orElseGet() calls its supplier only for an empty one. The article on orElse() vs orElseGet() shows a duplicate database row caused by that difference.
In a REST API, a missing record often becomes a 404 response. The service layer calls orElseThrow() with its own exception, which carries a useful message for the log and the error handler.
static Book requireBook(Catalog catalog, String title) {
return catalog.findByTitle(title)
.orElseThrow(() -> new NoSuchElementException("No book titled " + title));
}
Catalog catalog = sampleCatalog();
String title = requireBook(catalog, "Dune").title(); // "Dune"
Book hobbit = requireBook(catalog, "Hobbit"); // NoSuchElementException: No book titled Hobbit
The exception message names the missing title, so the log entry explains the 404 without a debugger. Spring Data repositories follow the same pattern, because findById() returns an Optional.
5. Transforming Values with map(), flatMap() and filter()
The map(), flatMap() and filter() methods work on the value only when it is present, and pass an empty Optional through unchanged. That lets us replace several nested null checks with one chain.
For example, to print the email of the member who borrowed a book, the code without Optional needs a check for the book, the borrower and the email.
static String borrowerEmailWithNullChecks(Map<String, Book> books, String title) {
Book book = books.get(title);
if (book != null) {
Member member = book.borrower();
if (member != null && member.email() != null) {
return member.email();
}
}
return "no email";
}
The Optional version reads top to bottom. Each step either returns the next value or ends the chain with an empty Optional, and the default appears once at the end.
static String borrowerEmail(Catalog catalog, String title) {
return catalog.findByTitle(title)
.flatMap(Book::currentBorrower)
.flatMap(Member::emailAddress)
.orElse("no email");
}
Catalog catalog = sampleCatalog();
String lokesh = borrowerEmail(catalog, "Dune"); // "lokesh@mail.com"
String ana = borrowerEmail(catalog, "Emma"); // "no email", Ana has no email
String shelf = borrowerEmail(catalog, "Ulysses"); // "no email", nobody borrowed it
String unknown = borrowerEmail(catalog, "Hobbit"); // "no email", no such book
We use map() when the function returns a plain value, and flatMap() when it returns an Optional itself. With map(Book::currentBorrower), the result type is Optional<Optional<Member>>, which flatMap() avoids. A function passed to map() that returns null produces an empty Optional.
Catalog catalog = sampleCatalog();
Optional<String> viaMap = catalog.findByTitle("Emma")
.map(Book::borrower)
.map(Member::email);
boolean noEmail = viaMap.isEmpty(); // true, map() turned null into empty
boolean borrowed = catalog.findByTitle("Ulysses")
.filter(b -> b.borrower() != null)
.isPresent(); // false
6. Combining Fallbacks and Streams with or() and stream()
Java 9 added two methods that connect an Optional to other lookups and to streams. The or() method returns another Optional when the first one is empty, and stream() turns an Optional into a stream of zero or one element.
A common use of or() is a two-level lookup. The app checks a small in-memory cache first and asks the catalog only on a cache miss, and both lookups return an Optional.
Catalog cache = new Catalog();
Catalog database = sampleCatalog();
Optional<Book> book = cache.findByTitle("Emma")
.or(() -> database.findByTitle("Emma"));
String title = book.map(Book::title).orElse("none"); // "Emma", found in the database
The stream() method helps when we have a list of lookups and want only the results that exist. With flatMap(Optional::stream), empty results disappear from a Java stream, so a report of borrowers needs no filter(Optional::isPresent) and map(Optional::get) pair.
Catalog catalog = sampleCatalog();
List<String> borrowers = Stream.of("Dune", "Emma", "Ulysses", "Hobbit")
.map(catalog::findByTitle)
.flatMap(Optional::stream)
.flatMap(b -> b.currentBorrower().stream())
.map(Member::name)
.toList();
List<String> names = borrowers; // [Lokesh, Ana]
7. Primitive Optionals
The classes OptionalInt, OptionalLong and OptionalDouble hold a primitive value without boxing it. Terminal operations of primitive streams, such as max() and average(), return them, because an empty stream has no maximum or average.
OptionalInt longest = Stream.of("Dune", "Emma", "Ulysses")
.mapToInt(String::length)
.max();
int length = longest.orElse(0); // 7
OptionalDouble noAverage = IntStream.empty().average();
boolean none = noAverage.isEmpty(); // true
Primitive optionals have orElse(), orElseThrow(), ifPresent() and stream(), but no map(), filter() or flatMap(). When we need those, we work on the stream before the terminal operation, or box the value into an Optional<Integer>.
8. When Not to Use Optional
Optional is a return type, so we keep it out of fields, parameters and collections. The JDK designers added it for the case where a method may have no result, and the class has rules that make it awkward elsewhere.
| Place | Problem | Use instead |
|---|---|---|
| Entity or DTO field | Optional is not Serializable, and JPA and JSON mappers expect plain fields | A nullable field with a getter that returns Optional.ofNullable(field) |
| Method or constructor parameter | Callers can still pass null, and every call site has to wrap its argument | Method overloading, or a nullable parameter with a clear Javadoc |
| Collection return type | Optional<List<T>> has two ways to say nothing | An empty list, such as List.of() |
| Elements of a collection or map | List<Optional<T>> forces every reader to unwrap | Leave missing elements out |
| Identity checks and locks | Optional is a value-based class, so == and synchronized are unreliable | equals(), and a dedicated lock object |
| Tight loops on hot paths | Each Optional may allocate an object | A plain null check inside private code |
A variable of type Optional must never be null itself. A method that returns null where an Optional is expected brings back the exception the class was meant to prevent.
Optional<Book> notFound = null;
boolean exists = notFound.isPresent(); // NullPointerException
The safe version returns Optional.empty() from every path of the method, so callers never need to check the Optional reference.
Optional<Book> notFound = Optional.empty();
boolean exists = notFound.isPresent(); // false
9. How Does Optional Work Internally?
The source of java.util.Optional is short. An Optional keeps the value in one final field, and a null field means the Optional is empty. All empty optionals share a single EMPTY instance, which is one reason why identity checks with == give misleading results.
// does not compile: excerpt from the JDK 25 source of java.util.Optional
private static final Optional<?> EMPTY = new Optional<>(null);
private final T value;
public static <T> Optional<T> of(T value) {
return new Optional<>(Objects.requireNonNull(value));
}
public T orElseThrow() {
if (value == null) {
throw new NoSuchElementException("No value present");
}
return value;
}
The constructor is private, so the factory methods from section 2 are the only way to get an instance. The other methods, such as map() and filter(), check the same value field and return EMPTY or a new Optional. The full code is in the OpenJDK repository.
10. Java Optional FAQs
Code reviews raise the same doubts about Optional whenever it shows up outside a return type.
10.1. Is Optional Serializable?
No. Optional does not implement Serializable, so a serializable class with an Optional field fails with NotSerializableException whenever the field is not null, even when it holds an empty Optional. We store the plain value in the field and wrap it in the getter.
10.2. Should I Use Optional as a Method Parameter?
No. A parameter of type Optional can still be null, so the method needs a null check anyway, and callers have to write Optional.of(x) or Optional.empty() at every call. Two overloaded methods, one with the argument and one without, are easier to call and to read.
10.3. What Is the Difference between Optional.of() and Optional.ofNullable()?
The of() method requires a non-null value and throws NullPointerException for null. The ofNullable() method accepts null and returns an empty Optional for it. We use of() for values that are never null by design, and ofNullable() to wrap results of APIs that may return null.
10.4. Why Does Spring Data findById() Return an Optional?
Because a record with the given ID may not exist, and the return type forces the caller to handle that case. A typical service method calls repository.findById(id).orElseThrow(() -> new BookNotFoundException(id)), and an exception handler maps the exception to a 404 response. The same idea appears in optional path variables in Spring MVC.
10.5. Is Optional Slower than a null Check?
Slightly, because a present Optional is an extra object, and each map() step may create another one. For service and repository methods the difference does not show up next to a database or network call. In a hot loop that runs millions of times, a private method can use a plain null check and keep Optional for its public API.
10.6. How Do I Convert an Optional to a List?
We call stream().toList(), which returns an empty list for an empty Optional and a one-element list otherwise. Both lists are unmodifiable.
List<String> one = Optional.of("Dune").stream().toList(); // [Dune]
List<String> zero = Optional.<String>empty().stream().toList(); // []
11. Conclusion
Optional tells the caller that a method may return no result, and its methods replace nested null checks with a chain that ends in one clear decision. We create it with of(), ofNullable() or empty(), transform it with map(), flatMap() and filter(), and finish with orElse(), orElseGet() or orElseThrow().
The Java 9 to 11 methods or(), stream(), ifPresentOrElse(), orElseThrow() and isEmpty() cover fallbacks, streams and checks that used to need get(). We keep Optional as a return type and leave fields, parameters and collections with plain values.
12. References
- Optional Javadoc (Java 25)
- OptionalInt Javadoc (Java 25)
- Value-based Classes
- JLS 4.1 The Kinds of Types and Values
Happy Learning !!
Actually Oracle advises to use Optional as class fields as well:
https://www.oracle.com/technical-resources/articles/java/java8-optional.html
so that the following piece of code
String version = &quot;UNKNOWN&quot;; if(computer != null){ Soundcard soundcard = computer.getSoundcard(); if(soundcard != null){ USB usb = soundcard.getUSB(); if(usb != null){ version = usb.getVersion(); } } }can be written as
String name = computer.flatMap(Computer::getSoundcard) .flatMap(Soundcard::getUSB) .map(USB::getVersion) .orElse(&quot;UNKNOWN&quot;);Thanks for sharing.
what about in case of object referance?