Null, Optional and NullPointerException
Why null causes so many Java bugs, how to read a helpful NullPointerException message, defensive checks with Objects, and when Optional makes absence explicit.
What null means
A variable of a reference type holds a reference to an object, or
the special value null: a reference to no object at
all. Fields of reference types start as null until
something is assigned (see
default field values), and methods often return null to mean "not found".
The general idea of a "no value" value is on
Types and Type Systems, including Tony Hoare's famous verdict on it.
public class Main {
public static void main(String[] args) {
String city = null;
Customer c = new Customer();
System.out.println(city);
System.out.println(c.email);
System.out.println(city == null);
System.out.println(city instanceof String);
System.out.println("City: " + city);
}
}
class Customer {
String email;
}
null null true false City: null
Printing, concatenating and comparing a null with
== are all fine; null instanceof anything
is false. Primitive types such as int and
boolean can never be null, but their
wrapper classes (Integer, Boolean) can,
which causes its own surprises (see
null and unboxing).
NullPointerException
A
NullPointerException
(often shortened to NPE) is the unchecked exception the JVM throws
when code uses null where an object is required:
calling a method on it, reading or writing a field, reading the
length of a null array or an element of it, unboxing a null
Integer to an int, or
throw null. Library methods also throw it on purpose
when you pass null where they do not accept it.
public class Main {
public static void main(String[] args) {
String email = findEmail("C-10043");
System.out.println("Looking up C-10043");
System.out.println(email.toLowerCase());
}
static String findEmail(String customerId) {
if (customerId.equals("C-10042")) {
return "Mei.Chen@example.com";
}
return null;
}
}
Looking up C-10043 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "<local1>" is null
The bug is not on the line that fails. It is in the design:
findEmail returns null for "unknown
customer", and nothing in its signature warns the caller. The rest
of this page is about both halves of the problem: reading the
message quickly, and designing code where null cannot
sneak through.
Reading a helpful NullPointerException
For a long time an NPE said only
java.lang.NullPointerException
and a line number, which is useless on a line like
a.b().c().d(). A
helpful NullPointerException
message names the operation that failed and the expression that was
null. The JVM computes it from the bytecode when the message is
first read. The feature arrived with JEP 358 in JDK 14 and has been
on by default since JDK 15.
public class Main {
public static void main(String[] args) {
Customer mei = new Customer("Mei Chen", new Address("Utrecht"));
Customer ravi = new Customer("Ravi Kumar", null);
System.out.println(mei.address().city());
System.out.println(ravi.address().city());
}
}
record Address(String city) {}
record Customer(String name, Address address) {}
Utrecht Exception in thread "main" java.lang.NullPointerException: Cannot invoke "Address.city()" because the return value of "Customer.address()" is null
Read the message in two parts:
-
What failed:
Cannot invoke "Address.city()". Other forms areCannot read field "name",Cannot read the array length,Cannot store to int arrayandCannot throw exception. -
What was null:
because the return value of "Customer.address()" is null. Ravi has no address on file; the bug is in the data or in the code that created the customer, not incity().
Unboxing is a classic hiding place. Map.get returns
null for a missing key, and adding that to an
int calls intValue() on it behind the
scenes:
import java.util.Map;
import java.util.TreeMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> usage = new TreeMap<>();
usage.put("E-001", 333);
usage.put("E-002", 412);
int total = usage.get("E-001");
System.out.println(total);
total += usage.get("E-003");
}
}
333 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
The code never calls intValue(), but the message does
mention it: helpful messages describe the bytecode, so they show
what the compiler generated, with full class names for most library
types.
Why it says <local1>
Fields and methods always appear by name, because their names are
part of the class file anyway. Names of local variables and
parameters are only stored when the code is compiled with debug
information (javac -g). Without it, the message shows
the variable's slot number instead: "<local1>" or
"<parameter1>". Plain javac without
-g and the source launcher (java Main.java, which runs the examples on this site) both leave the names out.
public class Main {
public static void main(String[] args) {
try {
String name = null;
System.out.println(name.length());
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
try {
printLength(null);
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
try {
int[] readings = null;
System.out.println(readings.length);
} catch (NullPointerException e) {
System.out.println(e.getMessage());
}
}
static void printLength(String text) {
System.out.println(text.length());
}
}
Cannot invoke "String.length()" because "<local1>" is null Cannot invoke "String.length()" because "<parameter1>" is null Cannot read the array length because "<local1>" is null
The number is the JVM's local-variable slot. In a static
main(String[] args), slot 0 holds args, so
the first local you declare is <local1>;
name and readings both get slot 1 because
each lives in its own block. In an instance method slot 0 is
this, and a long or
double takes two slots, so counting by hand is
error-prone: use the line number from the stack trace and look at
the expressions on that line. Code compiled with -g,
which many build configurations do, shows the real names:
javac -g Main.java
java Main
Looking up C-10043 Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.toLowerCase()" because "email" is null at Main.main(Main.java:5)
The option
-XX:-ShowCodeDetailsInExceptionMessages turns the
messages off again; getMessage() then returns
null. Leave them on: they cost nothing until an NPE
actually happens, and they save a lot of guessing when one does.
Checking for null
Where null is a legitimate value, check for it before
use, as close as possible to where it appears. Java has no
?. or ?? operators like C# and JavaScript,
but a few habits and the helpers in
java.util.Objects keep the checks short:
import java.util.Objects;
public class Main {
public static void main(String[] args) {
String status = null;
String city = null;
String phone = null;
System.out.println("RESOLVED".equals(status));
System.out.println(Objects.equals(status, null));
System.out.println(Objects.requireNonNullElse(city, "unknown"));
System.out.println(Objects.toString(phone, "n/a"));
System.out.println(Objects.isNull(phone));
System.out.println(status.equals("RESOLVED"));
}
}
false true unknown n/a true Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.equals(Object)" because "<local1>" is null
-
"RESOLVED".equals(status)puts the constant first: a literal is never null, andequals(null)returnsfalse. The reversed form on the last line throws. -
Objects.equals(a, b)compares two values that may both be null. -
Objects.requireNonNullElse(value, fallback)andObjects.toString(value, fallback)replace null with a default, the Java counterpart of the null-coalescing operator. -
A
switchthrows an NPE on a null value unless it has acase null(see null in switch).
Failing fast with Objects.requireNonNull
A null that slips into an object's field causes an NPE much later,
in code that has nothing to do with the mistake. The fix is to
refuse it at the door.
Objects.requireNonNull(value, message)
returns the value unchanged when it is not null, and throws a
NullPointerException with your message when it is. Use
it in constructors and at the start of public methods.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
Customer mei = new Customer("C-10042", "Mei Chen");
System.out.println(mei);
Customer broken = new Customer("C-10043", null);
System.out.println("never printed");
}
}
class Customer {
private final String id;
private final String name;
Customer(String id, String name) {
this.id = Objects.requireNonNull(id, "id must not be null");
this.name = Objects.requireNonNull(name, "name must not be null");
}
@Override
public String toString() {
return id + " " + name;
}
}
C-10042 Mei Chen Exception in thread "main" java.lang.NullPointerException: name must not be null
The stack trace now points at the line that created the broken
customer, which is where the real bug is. In a
record's compact constructor
the same check is one line per component. Throwing
NullPointerException here, rather than
IllegalArgumentException, is the convention the JDK
itself follows for null arguments.
Optional
Optional<T>
(Java 8, in java.util) is a small container that holds
either one value or nothing. Returned from a method, it makes
absence part of the signature:
Optional<Customer> findCustomer(String id) tells
every caller "there may be no customer", and the compiler will not
let them use the result as a Customer without
unwrapping it.
import java.util.List;
import java.util.Optional;
public class Main {
static final List<Customer> CUSTOMERS = List.of(
new Customer("C-10042", "Mei Chen", "mei.chen@example.com"),
new Customer("C-10043", "Ravi Kumar", null));
public static void main(String[] args) {
Optional<Customer> found = findCustomer("C-10042");
Optional<Customer> missing = findCustomer("C-99999");
System.out.println(found.isPresent() + " " + missing.isPresent());
System.out.println(found.map(Customer::name).orElse("unknown"));
System.out.println(missing.map(Customer::name).orElse("unknown"));
System.out.println(missing);
found.ifPresent(c -> System.out.println("Hello, " + c.name()));
}
static Optional<Customer> findCustomer(String id) {
for (Customer c : CUSTOMERS) {
if (c.id().equals(id)) {
return Optional.of(c);
}
}
return Optional.empty();
}
}
record Customer(String id, String name, String email) {}
true false Mei Chen unknown Optional.empty Hello, Mei Chen
Optional.of(value) wraps a value that must not be null,
Optional.empty() is the empty case, and
Optional.ofNullable(value) chooses between them.
map transforms the value if there is one and passes
emptiness through untouched, so a chain of steps needs no
if at all. The Customer::name and
c -> ... forms are method references and lambdas
(see
Lambdas, Functional Interfaces and Streams).
Chaining and failing
When a step itself returns an Optional, use
flatMap instead of map, or you end up with
an Optional<Optional<String>>. When absence
is an error for this caller, orElseThrow turns it into
an exception with a clear message. Streams return
Optional too, for example from
findFirst():
import java.util.List;
import java.util.Optional;
public class Main {
static final List<Customer> CUSTOMERS = List.of(
new Customer("C-10042", "Mei Chen", "mei.chen@example.com"),
new Customer("C-10043", "Ravi Kumar", null));
public static void main(String[] args) {
for (String id : List.of("C-10042", "C-10043", "C-99999")) {
String contact = findCustomer(id)
.flatMap(Customer::emailIfKnown)
.map(String::toUpperCase)
.orElse("no email on file");
System.out.println(id + ": " + contact);
}
Customer c = findCustomer("C-99999")
.orElseThrow(() -> new IllegalStateException("customer C-99999 not found"));
}
static Optional<Customer> findCustomer(String id) {
return CUSTOMERS.stream().filter(c -> c.id().equals(id)).findFirst();
}
}
record Customer(String id, String name, String email) {
Optional<String> emailIfKnown() {
return Optional.ofNullable(email);
}
}
C-10042: MEI.CHEN@EXAMPLE.COM C-10043: no email on file C-99999: no email on file Exception in thread "main" java.lang.IllegalStateException: customer C-99999 not found
Ravi exists but has no email, and C-99999 does not exist at all; the
chain handles both without a single null check. The plain
orElseThrow() without arguments throws
NoSuchElementException, exactly like
get():
import java.util.Optional;
public class Main {
public static void main(String[] args) {
String email = null;
System.out.println(Optional.ofNullable(email));
Optional<String> nothing = Optional.empty();
System.out.println(nothing.orElse("none"));
System.out.println(nothing.get());
}
}
Optional.empty none Exception in thread "main" java.util.NoSuchElementException: No value present
Using Optional well
Optional only helps when it is used for what it was
designed for. The JDK's documentation calls Optional
"primarily intended for use as a method return type"; the commonly
recommended rules built on that come down to this:
-
Use it as a return type for "may have no result".
It is not meant for fields, method parameters or collection
elements; there, plain
nullchecks or an empty collection are simpler. -
Never return
nullfrom a method declared to returnOptional. ReturnOptional.empty(). -
Avoid
isPresent()followed byget(). It is a null check in disguise; usemap,orElse,orElseGet,orElseThroworifPresentOrElse. -
For primitives,
OptionalInt,OptionalLongandOptionalDoubleavoid boxing; streams return them from methods such asmax()on anIntStream.
Optional.of itself refuses null, with a bare
NullPointerException without a message: it calls
Objects.requireNonNull, which creates the exception
with new NullPointerException(), and helpful messages
are only added to exceptions the JVM throws itself.
import java.util.Optional;
public class Main {
public static void main(String[] args) {
String email = null;
Optional<String> wrapped = Optional.of(email);
}
}
Exception in thread "main" java.lang.NullPointerException
import java.util.Optional;
public class Main {
public static void main(String[] args) {
Optional<String> cached = Optional.of("Mei Chen");
String a = cached.orElse(loadFromDatabase());
System.out.println(a);
String b = cached.orElseGet(() -> loadFromDatabase());
System.out.println(b);
}
static String loadFromDatabase() {
System.out.println(" (database query ran)");
return "Mei Chen";
}
}
(database query ran) Mei Chen Mei Chen
Null in collections and APIs
Collections are where null causes the most confusion,
because the rules differ per class. HashMap.get returns
null both for a missing key and for a key stored with
the value null; ArrayList accepts null
elements; the immutable collections from List.of,
Set.of and Map.of reject them outright
(see
Arrays and Collections).
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) {
Map<String, Integer> usage = new HashMap<>();
usage.put("E-001", 333);
System.out.println(usage.get("E-003"));
System.out.println(usage.getOrDefault("E-003", 0));
System.out.println(usage.containsKey("E-003"));
List<String> names = new ArrayList<>();
names.add(null);
System.out.println(names.size() + " " + names);
List<String> fixed = List.of("Mei Chen", null);
}
}
null 0 false 1 [null] Exception in thread "main" java.lang.NullPointerException
For your own methods the rule is simple:
a method that returns a collection returns an empty one, never
null. Callers can then loop over the result, ask its size or stream it
without a check:
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> none = openTickets("Delft");
System.out.println(none.size() + " open tickets in Delft");
for (String t : none) {
System.out.println(t);
}
System.out.println(openTickets("Leiden"));
}
static List<String> openTickets(String city) {
if (city.equals("Leiden")) {
return List.of("INC-40112", "INC-40118");
}
return List.of();
}
}
0 open tickets in Delft [INC-40112, INC-40118]
The same applies to strings where "nothing" is a normal value (an
empty string may be clearer than null) and to arrays
(return new String[0]). Keep null for the
places where you cannot avoid it, such as values read from a
database column that allows NULL, and convert it at that boundary.
Nullability annotations
Because the Java language does not track nullness, the ecosystem
does it with annotations. A
nullability annotation
such as @Nullable or @NonNull marks a
field, parameter or return type as "may be null" or "never null".
The compiler itself ignores them; IDEs and static analysis tools
(for example IntelliJ IDEA, NullAway or the Checker Framework) read
them and warn when a possibly-null value is used without a check.
The JDK does not ship such annotations. Several libraries have their own, with the same names in different packages; the JSpecify project is a joint effort by several of those tool makers to agree on one standard set. A typical style marks a whole package or class as "non-null unless annotated" and annotates only the exceptions:
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public class CustomerService {
// never returns null: callers need no check
public String displayName(Customer customer) {
return customer.name();
}
// may return null: tools warn when a caller uses the result unchecked
public @Nullable String findEmail(String customerId) {
return lookup(customerId);
}
private @Nullable String lookup(String customerId) {
return null;
}
}
The annotations are a library dependency (see
Packages, Modules and Build Tools) and only help when a tool checks them in the build. They work
well together with the rest of this page: annotate the boundaries,
validate incoming values with requireNonNull, and
return Optional or empty collections instead of
null.