Null Handling Strategies in Java

Naveen AnamaneniUpdated · 8 min read

Picture a school form with a box for "Middle name". Some students have one and some don't. If the office leaves the box blank for some students and writes "N/A", "none" or "-" for others, every person who reads the forms has to guess what each one means. In Java, null is that blank box: it can mean "not found", "not loaded yet", "not applicable" or "somebody forgot". Every place that reads the value must remember to check it, and one forgotten check causes a NullPointerException. In this lesson, you will learn practical strategies that decide where null is allowed and keep it out of the rest of your code.

Why Null Causes Trouble

null in Java is a special value that is used to mean that a reference points to no object. Calling a method or reading a field through a null reference throws a NullPointerException (NPE).

The trouble isn't null itself. It is that the type doesn't say whether null is possible. A method declared as Student find(String name) might return a student or might return null, and nothing in the code forces the caller to check. The NPE then happens far away from the place where the null was created, which makes it hard to trace.

The strategies below share one idea: decide about null at the boundary (when values enter a method or leave it), so the code inside never has to guess.

Decide at the boundary

Strategy 1: Fail Fast on Required Parameters

If a parameter must not be null, check it at the start with Objects.requireNonNull. The program then fails immediately, with a clear message, at the line that received the bad value, instead of much later somewhere else.

Student.java▶ Try it
import java.util.Objects;
 
public class Student {
    record Enrolment(String name, String className) {
        Enrolment {
            Objects.requireNonNull(name, "name must not be null");
            Objects.requireNonNull(className, "className must not be null");
        }
    }
 
    public static void main(String[] args) {
        System.out.println(new Enrolment("Navika", "10-A"));
        try {
            new Enrolment("Srihitha", null);
        } catch (NullPointerException e) {
            System.out.println("Rejected: " + e.getMessage());
        }
    }
}
Output
Enrolment[name=Navika, className=10-A]
Rejected: className must not be null

Explanation: the record's compact constructor checks both fields. The bad object is never created, so no other code ever sees a half-filled Enrolment. The message names the exact field that was wrong.

Strategy 2: Return Empty Collections, Never Null

A method that returns a List, Set or Map should return an empty collection when there is nothing to return. Callers can then loop over the result without any check.

Lookups.java▶ Try it
import java.util.List;
import java.util.Map;
import java.util.Optional;
 
public class Lookups {
    static final Map<String, List<String>> CLUBS = Map.of(
            "Navika", List.of("Chess", "Robotics"),
            "Srihitha", List.of("Music"));
 
    static List<String> clubsOf(String student) {
        return CLUBS.getOrDefault(student, List.of());
    }
 
    static Optional<String> firstClubOf(String student) {
        return clubsOf(student).stream().findFirst();
    }
 
    public static void main(String[] args) {
        for (String club : clubsOf("Advitha")) {
            System.out.println("Never printed: " + club);
        }
        System.out.println("Advitha's clubs: " + clubsOf("Advitha"));
        System.out.println("Navika's clubs: " + clubsOf("Navika"));
 
        System.out.println(firstClubOf("Navika").orElse("no club"));
        System.out.println(firstClubOf("Advitha").orElse("no club"));
    }
}
Output
Advitha's clubs: []
Navika's clubs: [Chess, Robotics]
Chess
no club

Explanation:

  1. getOrDefault(student, List.of()) returns an empty list for a student with no clubs, so the for loop simply runs zero times.
  2. firstClubOf returns Optional<String>. The return type itself tells the caller "this might be empty", and orElse supplies the fallback. This is Strategy 3.

Strategy 3: Optional for Return Values

Use Optional<T> as the return type of a method that may not find a value, such as findById or firstMatch. The rules from "Optional Best Practices" apply:

DoDon't
Return Optional from methods that may find nothingUse Optional for fields or method parameters
Use orElse, orElseGet, orElseThrow, map, ifPresentCall get() without checking
Return Optional.empty()Return null from a method declared to return Optional

Strategy 4: Replace Null with a Default Early

When a sensible default exists, replace null as soon as the value arrives, and the rest of the code never sees it.

MethodDescription
Objects.requireNonNullElse(value, defaultValue)It returns value, or defaultValue if value is null.
Objects.requireNonNullElseGet(value, supplier)It is the same, but the default is created only when needed.
map.getOrDefault(key, defaultValue)It returns the default when the key is missing.
Optional.ofNullable(value).orElse(defaultValue)It does the same job; useful in a chain of map calls.
Settings.java
String section = Objects.requireNonNullElse(form.section(), "A");
int busRoute = routes.getOrDefault(studentId, 0);   // 0 = walks to school

Strategy 5: The Null Object

Sometimes callers check for null only to skip some behavior: "if there is a discount, apply it". A Null Object is a real object that does nothing, so callers can call it without a check.

Billing.java▶ Try it
public class Billing {
    interface Discount {
        int apply(int fee);
 
        Discount NONE = fee -> fee;     // the Null Object
    }
 
    record PercentOff(int percent) implements Discount {
        public int apply(int fee) {
            return fee - fee * percent / 100;
        }
    }
 
    static Discount discountFor(String student) {
        return switch (student) {
            case "Navika" -> new PercentOff(10);   // sibling discount
            default -> Discount.NONE;
        };
    }
 
    public static void main(String[] args) {
        for (String s : new String[] {"Navika", "Advitha"}) {
            int fee = discountFor(s).apply(15_000);
            System.out.println(s + ": " + fee);
        }
    }
}
Output
Navika: 13500
Advitha: 15000

Explanation: discountFor never returns null. A student without a discount gets Discount.NONE, which returns the fee unchanged. The loop has no if (discount != null) at all.

Strategy 6: Document and Annotate

Make your intent visible:

  • Documentation: write "never returns null" or "returns an empty list if..." in the method's doc comment.
  • Annotations: libraries such as JSpecify provide @Nullable and @NonNull. IDEs and tools such as NullAway then warn you when you forget a check. Java itself doesn't check these annotations.
  • Records and final fields that are checked in the constructor can be trusted for the object's whole life.

In Practice

SituationStrategy
Constructor or public method parameter that is requiredObjects.requireNonNull
Method returning a list, set or mapEmpty collection
Method that searches for one thingOptional
Optional form field with a defaultrequireNonNullElse at the boundary
Optional behavior (discount, logger, listener)Null Object
Data from outside (JSON, database, user input)Check or convert it once, right where it enters

Common Mistakes

Returning null "just this once". One method that returns null for a list forces every caller to check, forever.

Optional as a field or parameter. It adds overhead and still allows the field itself to be null. Use it for return values.

Catching NullPointerException. It hides the real bug. Fix the place where null came from.

Checking for null everywhere. if (x != null) on every line makes code hard to read. Check once at the boundary instead.

Practice

  1. Easy: Change clubsOf in Lookups.java to return null for unknown students, run the program, and read the exception. Then change it back and explain why the empty list is better.
  2. Harder: Add a Logger interface with a method log(String message) and a Logger.SILENT Null Object. Write a FeeService that takes a Logger in its constructor (checked with requireNonNull) and logs each fee it calculates. Run it with a printing logger and with SILENT.

Quick check: Null handling

4 questions
1What should a method that returns a List return when there are no results?
2What does Objects.requireNonNull(x, "x") do when x is null?
3Where should Optional be used?
4What is a Null Object?

Key Takeaways

Was this tutorial helpful?

Lesson 539 of 578