The billion-dollar mistake: null
On This Lesson 11 topics
Lesson Overview
Some lookups find nothing. Searching a contact list for an id that does not exist, asking a map for a key it does not hold, reading a setting that nobody configured: in each case the method has no result to give. For most of Java's history the usual answer was null, a special value meaning "no object here".
This lesson shows why that answer causes so many bugs, and introduces the tool Java added to fix the problem: Optional<T>, a small container that holds either one value or nothing. Think of a gift box that may or may not contain a gift. The box is always there, so you can always hold it, but you must open it and check before you use what is inside.
Why this matters for backend work: every service looks things up (a user by id, an order by number), and every lookup can come back empty. How a method reports "nothing found" decides whether the caller is warned at compile time or crashes in production.
This lesson builds on Map, on exceptions, on generics (BEG-031: Optional<String> is a generic class like Box<T>), and on lambdas (BEG-035). The next lesson, "Using Optional well", teaches how to transform and unwrap an Optional.
Everything below was compiled and run with JDK 21.
Learning Objectives
By the end of this lesson you will be able to:
- Explain what
nullmeans and what Java does when you use it as if it were an object. - Read a
NullPointerExceptionmessage and find the variable that wasnull. - Explain why a method signature such as
String findName(int id)hides the possibility of "no result". - Create an
OptionalwithOptional.of,Optional.empty, andOptional.ofNullable, and say when to use each. - Write a method that returns
Optional<T>when a result may be missing. - Check an
OptionalwithisPresentandisEmpty.
Written Explanation
What null is
A variable of a class type, such as String or Contact, does not hold the object itself. It holds a reference, a way to reach the object. null is the reference that points to nothing. You can assign it, pass it around, and return it from methods, and the compiler accepts all of that.
The trouble starts when you use it as if it were an object. Calling a method on null has no object to run on, so Java throws a NullPointerException, usually shortened to NPE. You met exceptions in the exceptions module: the NPE is an unchecked exception, so the compiler never forces you to deal with it.
Why null is called the billion-dollar mistake
Tony Hoare, the computer scientist who introduced null references into a programming language in the 1960s, later called that decision his "billion-dollar mistake". He said he did it because it was easy to implement, and that it led to countless errors and crashes. The "billion" is a rhetorical estimate of the cost, not a measured figure, but the problem it points at is real and you will meet it often.
Here is what that looks like in Java:
static String findName(int id) {
return names.get(id); // null when the id is unknown
}
String name = findName(7);
System.out.println(name.length()); // NullPointerExceptionThree things make this dangerous:
- The signature does not warn you.
String findName(int id)looks the same whether or not it can returnnull. The compiler treats the result like any otherString. - The caller can simply forget the check. Nothing forces it, so the code compiles and often works in testing, where the ids exist.
- The crash happens far from the cause. The method that returned
nullis long finished by the time somebody calls.length()on it. Anullthat travels through a few variables and fields can crash in a place that looks innocent.
The JDK 21 used here names the thing that was null in the message, for example because "name" is null. Local variable names appear only if the class was compiled with javac -g; otherwise you see a placeholder such as <local1>.
Optional<T>: a box that may be empty
Optional<T> lives in java.util. It is a container that either holds one non-null value of type T or holds nothing. Its Javadoc says it is primarily meant as a method return type for cases where there is a clear need to represent "no result" and where null is likely to cause errors.
The key idea is that "might be empty" becomes part of the type. A method declared as
static Optional<String> findName(int id)tells every caller, in the signature itself, that there may be no name. A caller cannot call .length() on an Optional<String>: the compiler rejects it, because Optional has no length method. The caller must first open the box.
Creating an Optional
There are three ways to create one, and choosing correctly matters:
| Call | Use it when | If the argument is null |
|---|---|---|
Optional.of(value) | you are sure the value exists | throws NullPointerException |
Optional.empty() | you know there is no result | (takes no argument) |
Optional.ofNullable(value) | the value may be null, for example from Map.get | returns an empty Optional |
Optional.of(null) failing immediately is deliberate: it turns a hidden bug into a visible one at the exact line where it happens, instead of a crash later.
When you already have a value that may be null (the classic case is map.get(key)), ofNullable is the bridge between the old style and the new one.
Checking an Optional
The simplest way to look inside is two methods:
isPresent()returnstrueif there is a value.isEmpty()returnstrueif there is no value. It is the opposite ofisPresent()and was added in Java 11.
Printing an Optional shows Optional[Ada] or Optional.empty. The Javadoc leaves the exact toString format unspecified, so use it for debugging only.
There is also get(), which returns the value. On an empty Optional it throws NoSuchElementException, a crash in a new disguise. The next lessons show better ways to unwrap. In this lesson we use get() only right after an isPresent() check.
Do not compare an Optional with == to Optional.empty(). The Javadoc says there is no guarantee that the empty instance is a single shared object, so use isEmpty() or isPresent().
Examples
Example 1: the crash (simple)
import java.util.Map;
public class Ex1 {
static Map<Integer, String> names = Map.of(1, "Ada", 2, "Grace");
static String findName(int id) {
return names.get(id); // null when the id is unknown
}
public static void main(String[] args) {
String name = findName(7);
System.out.println("Looking up 7...");
System.out.println(name.length());
}
}Compiled with javac -g Ex1.java, running it prints:
Looking up 7...
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "name" is null
at Ex1.main(Ex1.java:13)Read the message like a sentence: Java tried to invoke String.length() on the variable name, and name was null. The stack trace line tells you where (Ex1.java, line 13). The fix is not at line 13 though: the real question is why findName returned null, and the answer is that id 7 is not in the map.
Example 2: returning an Optional (practical)
import java.util.Optional;
public class Ex2 {
static Optional<String> findName(int id) {
if (id == 1) return Optional.of("Ada");
return Optional.empty();
}
public static void main(String[] args) {
Optional<String> a = findName(1);
Optional<String> b = findName(7);
System.out.println(a);
System.out.println(b);
System.out.println(a.isPresent() + " " + b.isPresent() + " " + b.isEmpty());
if (a.isPresent()) {
System.out.println(a.get().length());
}
System.out.println(Optional.ofNullable(null));
System.out.println(Optional.of(null));
}
}Output:
Optional[Ada]
Optional.empty
true false true
3
Optional.empty
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:233)
at java.base/java.util.Optional.of(Optional.java:113)
at Ex2.main(Ex2.java:20)Both outcomes of findName have the same type, so the caller handles one return type either way. The last line shows Optional.of(null) failing at once, in the line that passed null. (The line numbers inside the JDK may differ between JDK versions.)
If you try to use the result as a plain String, the compiler stops you:
Ex4.java:5: error: incompatible types: Optional<String> cannot be converted to String
String n = findName(1);
^
Ex4.java:7: error: cannot find symbol
System.out.println(o.length());
^
symbol: method length()
location: variable o of type Optional<String>This is the whole point: the "forgot to check" mistake, which was a runtime crash with null, becomes a compile error.
Example 3: a lookup in a repository (realistic)
A Map returns null for a missing key, so the repository wraps the answer with ofNullable:
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
public class Ex3 {
record Contact(String name, String email) { }
static Map<Integer, Contact> store = new LinkedHashMap<>();
static Optional<Contact> findById(int id) {
return Optional.ofNullable(store.get(id));
}
public static void main(String[] args) {
store.put(1, new Contact("Ada", "ada@example.com"));
System.out.println(findById(1));
System.out.println(findById(9));
Optional<Contact> c = findById(9);
System.out.println(c.isPresent() ? c.get().name() : "no such contact");
}
}Output:
Optional[Contact[name=Ada, email=ada@example.com]]
Optional.empty
no such contactContact is a record, used only as a quick data holder as in earlier modules. The method body is one line, and Optional.ofNullable(store.get(id)) is the standard recipe for converting a null-returning lookup. This is exactly the change you will make to the generic repository's findById in the demo lesson of this module.
Backend Insight
Backend code is full of "find one thing" calls: find the user for this login, the order for this number, the price for this product. Frameworks you will meet later, such as Spring Data, use Optional as the return type of their "find by id" methods for exactly this reason. Knowing it now means those signatures will already look familiar.
One practical effect in a real service:
- A missing id is normal, not exceptional. A client asks for order 9999 that does not exist. With
Optionalthe code that calls the repository must decide what that means: usually an HTTP "not found" response. Withnull, the decision is easy to skip and the first sign is a crash that looks like a server bug rather than a client mistake.
Common Mistakes
Mistake 1: using Optional.of on a value that may be null
- Incorrect:
return Optional.of(store.get(id)); - Correct:
return Optional.ofNullable(store.get(id));
of throws NullPointerException when given null, so the "missing id" case crashes inside your own repository. Use of only for values you have just created or checked.
Mistake 2: returning null from a method whose type is Optional
- Incorrect:
static Optional<String> findName(int id) { return null; } - Correct:
return Optional.empty();
The type promises a box, and the caller never checks the box for null. Returning null brings back the very problem the type was meant to solve. The Javadoc says an Optional variable should never itself be null.
Mistake 3: comparing with ==
- Incorrect:
if (result == Optional.empty()) { ... } - Correct:
if (result.isEmpty()) { ... }
The Javadoc says there is no guarantee that Optional.empty() is a single shared instance, so identity comparison is unreliable.
Exercises
Try each exercise yourself before opening the solutions. Where you write code, compile and run it. Solutions were compiled with JDK 21.
Level A: Foundation
A1. What does null mean for a variable of type String? What does Java do when you call .trim() on it?
Show solution A1
A1. null means the variable refers to no object. Calling .trim() on it throws NullPointerException, because there is no object to run the method on.
A2. For each call, say what happens, assuming text is null: (a) Optional.of(text), (b) Optional.ofNullable(text), (c) Optional.empty().
Show solution A2
A2. (a) Throws NullPointerException immediately. (b) Returns an empty Optional. (c) Returns an empty Optional; there is no argument.
A3. A method is declared User findUser(String email). Name two things a caller cannot learn from this signature that matter if the user may not exist.
Show solution A3
A3. The signature does not say whether the method can return null, and it does not say what the caller should do in that case. The compiler therefore does not force the check.
Level B: Application
B1. Rewrite this method so it never returns null. It should return Optional<String>:
static String capitalOf(String country) {
if (country.equals("France")) return "Paris";
if (country.equals("Japan")) return "Tokyo";
return null;
}Show solution B1
B1.
static Optional<String> capitalOf(String country) {
if (country.equals("France")) return Optional.of("Paris");
if (country.equals("Japan")) return Optional.of("Tokyo");
return Optional.empty();
}capitalOf("Japan") prints Optional[Tokyo] and capitalOf("Peru") prints Optional.empty. Note that country itself could still be null: this method does not protect against that.
B2. Write Optional<String> firstStartingWith(List<String> words, String prefix). It returns the first word with that prefix, or an empty Optional. Use a plain for loop.
Show solution B2
B2.
static Optional<String> firstStartingWith(List<String> words, String prefix) {
for (String w : words) {
if (w.startsWith(prefix)) return Optional.of(w);
}
return Optional.empty();
}For List.of("kiwi", "plum", "peach") and prefix "p" the result is Optional[plum]; for List.of("kiwi") and prefix "z" it is Optional.empty.
B3. Predict the output of each line, then run it:
Optional<String> a = Optional.of("");
System.out.println(a.isPresent());
String s = null;
System.out.println(Optional.ofNullable(s).isEmpty());Show solution B3
B3. The first line prints true: the empty string is a real value, not null, so the Optional is present. The second line prints true: ofNullable(null) gives an empty Optional.
Level C: Challenge
C1. Write Optional<Integer> parseAge(String text). It should return the age as an Optional<Integer> when text is a whole number that is zero or more (ignore spaces around it), and an empty Optional otherwise (not a number, or negative). Hint: Integer.parseInt throws NumberFormatException for bad input.
Show solution C1
C1.
static Optional<Integer> parseAge(String text) {
try {
int age = Integer.parseInt(text.trim());
if (age >= 0) return Optional.of(age);
return Optional.empty();
} catch (NumberFormatException e) {
return Optional.empty();
}
}parseAge(" 42 ") gives Optional[42], and parseAge("abc") and parseAge("-5") both give Optional.empty. A null argument would still throw NullPointerException at text.trim().
C2. A repository method Contact findById(int id) returns store.get(id). A caller prints repo.findById(5).name(). Explain what happens for a missing id today, then change the method and the caller so the compiler forces the caller to deal with the missing case. Why is the new version safer, and what does it still not guarantee?
Show solution C2
C2. Today, a missing id makes findById return null, and .name() is called on null, so the program throws NullPointerException. New version:
Optional<Contact> findById(int id) {
return Optional.ofNullable(store.get(id));
}
// caller
Optional<Contact> found = repo.findById(5);
System.out.println(found.isPresent() ? found.get().name() : "no such contact");The caller can no longer write repo.findById(5).name(): it does not compile, because Optional has no name(). It is safer because the possible absence is in the type. It still does not guarantee that the caller handles the empty case well (it could print nothing, or pick a bad default), only that it has to see the case.
Mini Challenge
Write a small class Catalog that stores products in a Map<String, Double> of product name to price (put three products in). Give it a method Optional<Double> priceOf(String name) that wraps the map lookup with ofNullable. Then write a main that:
- Prints the result for one product that exists and one that does not.
- Prints a friendly sentence for each, using
isPresentorisEmpty. - Includes, as a comment, the exact compiler error you get when you try to assign
priceOf("tea")to a plaindouble.
Check your work: no method returns null, you used ofNullable (not of) around map.get, and every get() call sits right after a check.
Summary & Next Lesson
Key takeaways
nullmeans "no object". Calling a method on it throwsNullPointerException, which the compiler never forces you to handle.- A signature like
String findName(int id)hides the chance of "no result", so callers forget the check and crash far from the cause. Optional<T>makes "might be empty" part of the type. It is meant mainly as a return type.Optional.of(x)requires a non-nullx,Optional.empty()is the empty box, andOptional.ofNullable(x)acceptsnulland gives an empty box for it.- Check with
isPresent()andisEmpty(). A bareget()on an emptyOptionalthrowsNoSuchElementException.
Next lesson
The next lesson, "Using Optional well", shows how to work with an Optional without repeated if statements: transform it with map, filter it, and unwrap it with orElse, orElseGet, and orElseThrow.
Self-check
- What exception does Java throw when you call a method on
null? - Why is
Optional.of(map.get(key))risky? - Which factory method converts a possibly-
nullvalue into anOptional? - What type error do you get if you assign an
Optional<String>to aStringvariable? - Why should you not write
if (result == Optional.empty())?
Show answers
NullPointerException.- If the key is missing,
map.get(key)isnull, andOptional.of(null)throwsNullPointerException. UseofNullableinstead. Optional.ofNullable.- A compile error:
incompatible types: Optional<String> cannot be converted to String. - Identity comparison is unreliable, because the Javadoc gives no guarantee that the empty
Optionalis a single shared instance. UseisEmpty()orisPresent().
References
- Oracle, Java SE 21 API:
Optional. Supports the intended use as a return type, the behaviour ofof,ofNullable, andempty,getthrowingNoSuchElementException,isEmpty(Java 11), the unspecifiedtoStringformat, the advice not to compare with==, and that anOptionalvariable should never benull. - Oracle, Java SE 21 API:
NoSuchElementException. Supports its meaning: a requested element does not exist. - InfoQ, Null References: The Billion Dollar Mistake (Tony Hoare). Supports the origin of the phrase and Hoare's reason (ease of implementation). The page also states the cost figure is only an estimate.
- Not verified from documentation: the helpful NPE message format and the effect of
-g; both were observed by running the code on JDK 21.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.