Null Handling Strategies in Java
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.
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.
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());
}
}
}Enrolment[name=Navika, className=10-A]
Rejected: className must not be nullExplanation: 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.
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"));
}
}Advitha's clubs: []
Navika's clubs: [Chess, Robotics]
Chess
no clubExplanation:
getOrDefault(student, List.of())returns an empty list for a student with no clubs, so theforloop simply runs zero times.firstClubOfreturnsOptional<String>. The return type itself tells the caller "this might be empty", andorElsesupplies 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:
| Do | Don't |
|---|---|
Return Optional from methods that may find nothing | Use Optional for fields or method parameters |
Use orElse, orElseGet, orElseThrow, map, ifPresent | Call 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.
| Method | Description |
|---|---|
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. |
String section = Objects.requireNonNullElse(form.section(), "A");
int busRoute = routes.getOrDefault(studentId, 0); // 0 = walks to schoolStrategy 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.
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);
}
}
}Navika: 13500
Advitha: 15000Explanation: 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
@Nullableand@NonNull. IDEs and tools such as NullAway then warn you when you forget a check. Java itself doesn't check these annotations. - Records and
finalfields that are checked in the constructor can be trusted for the object's whole life.
In Practice
| Situation | Strategy |
|---|---|
| Constructor or public method parameter that is required | Objects.requireNonNull |
| Method returning a list, set or map | Empty collection |
| Method that searches for one thing | Optional |
| Optional form field with a default | requireNonNullElse 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
- Easy: Change
clubsOfinLookups.javato returnnullfor unknown students, run the program, and read the exception. Then change it back and explain why the empty list is better. - Harder: Add a
Loggerinterface with a methodlog(String message)and aLogger.SILENTNull Object. Write aFeeServicethat takes aLoggerin its constructor (checked withrequireNonNull) and logs each fee it calculates. Run it with a printing logger and withSILENT.
Key Takeaways
Was this tutorial helpful?