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 are Cannot read field "name", Cannot read the array length, Cannot store to int array and Cannot 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 in city().

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, and equals(null) returns false. The reversed form on the last line throws.
  • Objects.equals(a, b) compares two values that may both be null.
  • Objects.requireNonNullElse(value, fallback) and Objects.toString(value, fallback) replace null with a default, the Java counterpart of the null-coalescing operator.
  • A switch throws an NPE on a null value unless it has a case 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 null checks or an empty collection are simpler.
  • Never return null from a method declared to return Optional. Return Optional.empty().
  • Avoid isPresent() followed by get(). It is a null check in disguise; use map, orElse, orElseGet, orElseThrow or ifPresentOrElse.
  • For primitives, OptionalInt, OptionalLong and OptionalDouble avoid boxing; streams return them from methods such as max() on an IntStream.

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.