How to handle (and avoid) NullPointerExceptions in Java
A guide on the different developer practices, language features, libraries, and patterns that you can use to prevent NullPointerExceptions.
Sep 11, 2024 • 7 Minute Read
A recent study demonstrated that NullPointerExceptions are found in 70% of production payloads. This easily makes it the most common exception in the Java language.
In this article, you’ll learn the several strategies, language features, and third-party libraries at your disposal to help you address null pointers before they happen and troubleshoot them faster when they do.
What is a NullPointerException?
Java throws a NullPointerException when an application attempts to use a field or method on a null reference. For example, Java will throw a NullPointerException in the following code:
Soccer ball = null;
ball.shoot(); // Java throws a NullPointerException here
But it can also happen in more subtle situations like this one:
Boolean willVote = null;
if (willVote) { // will throw NullPointerException
Because of this and other subtleties, it can be hard to realize at the time you are writing code where null pointers may occur. Check out the following strategies to make it easier.
Validating your inputs
Let’s begin with good programmer hygiene. One important practice is validating inputs. And while this is important for other reasons like security, it can help with null pointers as well.
It’s often the case that a method can provide more contextualized information about itself than the Java runtime can. So, if your method doesn’t support nulls, you can write code to say so and use the context you have to help:
public String readFileAsString(String filename) {
if (filename == null) {
throw new IllegalArgumentException(“Failed to read null filename. Please specify the name of the file to read.”);
Because this method checks for null it can give a more contextualized exception; IllegalArgumentException indicates that the responsibility lies with the caller, not the callee, to repair the issue. It also gives a more detailed error message than the generic one that NullPointerException would have used.
Check For Null In Constructors
If you check for null in constructors, too, then it provides a powerful network effect on your codebase. For example, if you write your constructor this way:
public Widget(Cog cog, List<Wheel> wheel, Integer torque) {
this.cog = cog;
this.wheel = wheel;
this.torque = torque;
}
then when those values widget.cog, widget.wheel, and widget.torque are read anywhere else, those places will need to check for null.
But, if you check for null in the constructor like so:
public Widget(Cog cog, List<Wheel> wheel, Integer torque) {
if (cog == null) { throw new IllegalArgumentException(“cog cannot be null”) }
if (wheel == null) { …
// …
}
then these null checks only need to happen in this one location. The rest of the codebase can now read widget.cog, for example, knowing that it isn’t null.
Break Up Nullable Invocations Onto Separate Lines
Another hygienic practice is to break up invocation chains like this one:
university.getCollege(“engineering”).getStudents().iterator().next()
into something more readable and debuggable like this:
College college = university.getCollege(“engineering”);
List<Student> students = college.getStudents();
Student student = students.iterator().next();
This is helpful with null pointers when the chain contains method invocations that might return null.
Consider the sample where everything is on one line. While that may be less typing for the developer at the time, it makes it more challenging if a NullPointerException happens. Even though the exception gives the line where the null pointer is, you don’t know whether university is null, or the return of getCollege or getStudents.
In the second code block, it will be clearer where the null pointer is because each nullable call is on its own line, making the stack traces unique.
Perform Unit and Integration Testing
The practice of unit testing is also generally important and can come to your aid when preventing null pointers.
For example, consider a test for Widget, our class from earlier, that ensures it is robust against null usage:
@Test
public givenConstructorWhenNullParametersThenException() {
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
new Widget(null, mockWheels, mockTorque));
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
new Widget(mockCog, null, mockTorque));
assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
new Widget(mockCog, mockWheels, null));
}
When adhering to this practice, you can find out during development time when you have introduced null unsafe behavior into your application.
Use @Nonnull and @Nullable Annotations
JEP 305 introduced a standard that allows for method signatures to indicate whether parameters and return values are null. Adding this clarity to a method signature makes it easier for developers to use your method correctly. It also makes it easier for IDEs to alert developers to incorrect or unsafe usage.
Consider, for example, the University#getCollege example from earlier. It’s implementation might be something like this:
public College getCollege(String name) {
return this.colleges.get(name);
}
If this.colleges is a Map, then get could return null. getCollege has two choices to make itself more readable and safer to use.
On the one hand, it can indicate that the return might be null like so:
@Nullable
public College getCollege(String name) {
// …
}
This indicates to the developer and IDEs that getCollege might return a null value. This allows an IDE to look at something like this:
university.getCollege(“engineering”).getStudents().iterator().next()
and confidently alert that it could be a problem.
On the other hand, it can make itself null-safe and then use @NonNull like so:
@NonNull
public College getCollege(String name) {
return this.colleges.computeIfAbsent(name, (n) -> new College(n));
}
Because computeIfAbsent is null-safe, then now so is getCollege.
Because of that, something like:
university.getCollege(“engineering”).getStudents()
is now known to be safe to do.
Use Null-Safe Libraries
There are several libraries that follow the above practices and expose null-safe APIs.
For example, Apache Commons and Google Guava have null-safe string checks like isNotEmpty:
if (StringUtils.isNotEmpty(possiblyNullString)) {
// …
}
This utility method is handy because it is easier to read and remember than:
if (possiblyNullString == null || possiblyNullString.length() == 0) {
// …
}
When validating inputs, null-safe APIs like the above can ultimately simplify your code as well. Considering the readFileAsString method from earlier, you can use the above to check for null and check for emptiness, since both would be invalid in that case:
public String readFileAsString(String filename) {
if (StringUtils.isEmpty(filename)) {
throw new IllegalArgumentException(“Failed to read null filename. Please specify the name of the file to read.”);
Be advised that there is a trade-off when using null-safe libraries, though. Just because they are checking for null, it doesn’t mean that null is a valid value for your API. If null is an illegal argument for your API, you should still error instead of silently failing.
Use Optional
Sometimes null is a valid value and Java’s Optional API can help in many ways.
Perhaps the most common version of this is when there is a reasonable default value and null indicates that the callee should choose the default. For example, consider the university code from earlier:
university.getCollege(“engineering”).getStudents().iterator().next()
Let’s imagine that getCollege is implemented like this:
@Nullable
public College getCollege(String name) {
return this.colleges.get(name);
}
A way to address the potential of a null return value would be to have it return Optional instead like so:
@NonNull
public Optional<College> getCollege(String name) {
return Optional.ofNullable(this.colleges.get(name));
}
Now, you can call things differently, knowing that you are protected against null values:
university.getCollege(“engineering”)
.map(College::getStudents)
.map((students) -> students.iterator().next())
.orElse(null);
Now, the University class is indicating that null might be returned and the calling code can accommodate that with a nice functional approach.
Follow the Null Object Pattern
A more advanced way to return a null-safe value is to follow Null Object Pattern. This is a pattern from Object-Oriented Programming (OOP) that allows for returning a non-null, usually no-op instance of the return value.
So, another way to implement a null-safe getCollege is:
@NonNull
public College getCollege(String name) {
College college = this.colleges.get(name);
if (college == null) {
return new NotFoundCollege();
}
return college;
}
where NotFoundCollege#getStudents would always return an empty array of students.
Note that Null Object Pattern is a more advanced pattern and should not be used to propagate validation that is more appropriately dealt with at the call site. For example, it would also be reasonable to throw an exception in getCollege:
@NonNull
public College getCollege(String name) {
College college = this.colleges.get(name);
if (college == null) {
throw new CollegeNotFoundException();
}
return college;
}
and this would also be a way to make getCollege null-safe.
Conclusion
In this article, we examined different developer practices, language features, libraries, and patterns that you can use to prevent NullPointerExceptions. Many of them are practices that developers should be following anyway for myriad reasons and can be applied liberally across your codebase. More advanced ones like Optional and Null Object Pattern require experience and care to know when each usage is appropriate. Master each one to make your code more robust and null-free!