52. Using the enhanced NullPointerException
Take your time to dissect the following trivial code and try to identify the parts that are prone to cause a NullPointerException
(these parts are marked as numbered warnings, which will be explained after the snippet):
public final class ChainSaw {
private static final List<String> MODELS
= List.of("T300", "T450", "T700", "T800", "T900");
private final String model;
private final String power;
private final int speed;
public boolean started;
private ChainSaw(String model, String power, int speed) {
this.model = model;
this.power = power;
this.speed = speed;
}
public static ChainSaw initChainSaw(String model) {
for (String m : MODELS) {
if (model.endsWith(m)) {WARNING 3!
return new ChainSaw(model, null, WARNING 5!
(int) (Math.random() * 100));
}
}
return null; WARNING 1,2!
}
public int performance(ChainSaw[] css) {
int score = 0;
for (ChainSaw cs : css) { WARNING 3!
score += Integer.compare(
this.speed,cs.speed); WARNING 4!
}
return score;
}
public void start() {
if (!started) {
System.out.println("Started ...");
started = true;
}
}
public void stop() {
if (started) {
System.out.println("Stopped ...");
started = false;
}
}
public String getPower() {
return power; WARNING 5!
}
@Override
public String toString() {
return "ChainSaw{" + "model=" + model
+ ", speed=" + speed + ", started=" + started + '}';
}
}
You noticed the warnings? Of course, you did! There are five major scenarios behind most NullPointerException (NPEs) and each of them is present in the previous class. Prior to JDK 14, an NPE doesn’t contain detailed information about the cause. Look at this exception:
Exception in thread "main" java.lang.NullPointerException
at modern.challenge.Main.main(Main.java:21)
This message is just a starting point for the debugging process. We don’t know the root cause of this NPE or which variable is null
. But, starting with JDK 14 (JEP 358), we have really helpful NPE messages. For example, in JDK 14+, the previous message looks as follows:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "modern.challenge.Strings.reverse()" because "str" is null
at modern.challenge.Main.main(Main.java:21)
The highlighted part of the message gives us important information about the root cause of this NPE. Now, we know that the str
variable is null
, so no need to debug further. We can just focus on how to fix this issue.
Next, let’s tackle each of the five major root causes of NPEs.
WARNING 1! NPE when calling an instance method via a null object
Consider the following code written by a client of ChainSaw
:
ChainSaw cs = ChainSaw.initChainSaw("QW-T650");
cs.start(); // 'cs' is null
The client passes a chainsaw model that is not supported by this class, so the initChainSaw()
method returns null
. This is really bad because every time the client uses the cs
variable, they will get back an NPE as follows:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "modern.challenge.ChainSaw.start()" because "cs" is null
at modern.challenge.Main.main(Main.java:9)
Instead of returning null
, it is better to throw an explicit exception that informs the client that they cannot continue because we don’t have this chainsaw model (we can go for the classical IllegalArgumentException
or, the more suggestive one in this case (but quite uncommon for null
value handling), UnsupportedOperationException
). This may be the proper fix in this case, but it is not universally true. There are cases when it is better to return an empty object (for example, an empty string, collection, or array) or a default object (for example, an object with minimalist settings) that doesn’t break the client code. Since JDK 8, we can use Optional
as well. Of course, there are cases when returning null
makes sense but that is more common in APIs and special situations.
WARNING 2! NPE when accessing (or modifying) the field of a null object
Consider the following code written by a client of ChainSaw
:
ChainSaw cs = ChainSaw.initChainSaw("QW-T650");
boolean isStarted = cs.started; // 'cs' is null
Practically, the NPE, in this case, has the same root cause as the previous case. We try to access the started
field of ChainSaw
. Since this is a primitive boolean
, it was initialized by JVM with false
, but we cannot “see” that since we try to access this field through a null
variable represented by cs
.
WARNING 3! NPE when null is passed in the method argument
Consider the following code written by a client of ChainSaw
:
ChainSaw cs = ChainSaw.initChainSaw(null);
You are not a good citizen if you want a null ChainSaw
, but who am I to judge? It is possible for this to happen and will lead to the following NPE:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.endsWith(String)" because "model" is null
at modern.challenge.ChainSaw.initChainSaw(ChainSaw.java:25)
at modern.challenge.Main.main(Main.java:16)
The message is crystal clear. We attempt to call the String.endWith()
method with a null
argument represented by the model
variable. To fix this issue, we have to add a guard condition to ensure that the passed model
argument is not null
(and eventually, not empty). In this case, we can throw an IllegalArgumentException
to inform the client that we are here and we are guarding. Another approach may consist of replacing the given null
with a dummy model that passes through our code without issues (for instance, since the model is a String
, we can reassign an empty string, ““). However, personally, I don’t recommend this approach, not even for small methods. You never know how the code will evolve and such dummy reassignments can lead to brittle code.
WARNING 4! NPE when accessing the index value of a null array/collection
Consider the following code written by a client of ChainSaw
:
ChainSaw myChainSaw = ChainSaw.initChainSaw("QWE-T800");
ChainSaw[] friendsChainSaw = new ChainSaw[]{
ChainSaw.initChainSaw("Q22-T450"),
ChainSaw.initChainSaw("QRT-T300"),
ChainSaw.initChainSaw("Q-T900"),
null, // ops!
ChainSaw.initChainSaw("QMM-T850"), // model is not supported
ChainSaw.initChainSaw("ASR-T900")
};
int score = myChainSaw.performance(friendsChainSaw);
Creating an array of ChainSaw
was quite challenging in this example. We accidentally slipped a null
value (actually, we did it intentionally) and an unsupported model. In return, we get the following NPE:
Exception in thread "main" java.lang.NullPointerException: Cannot read field "speed" because "cs" is null
at modern.challenge.ChainSaw.performance(ChainSaw.java:37)
at modern.challenge.Main.main(Main.java:31)
The message informs us that the cs
variable is null
. This is happening at line 37 in ChainSaw
, so in the for loop of the performance()
method. While looping the given array, our code iterated over the null
value, which doesn’t have the speed
field. Pay attention to this kind of scenario: even if the given array/collection itself is not null
, it doesn’t mean that it cannot contain null
items. So, adding a guarding check before handling each item can save us from an NPE in this case. Depending on the context, we can throw an IllegalArgumentException
when the loop passes over the first null
or simply ignore null
values and don’t break the flow (in general, this is more suitable). Of course, using a collection that doesn’t accept null
values is also a good approach (Apache Commons Collection and Guava have such collections).
WARNING 5! NPE when accessing a field via a getter
Consider the following code written by a client of ChainSaw
:
ChainSaw cs = ChainSaw.initChainSaw("T5A-T800");
String power = cs.getPower();
System.out.println(power.concat(" Watts"));
And, the associated NPE:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.concat(String)" because "power" is null
at modern.challenge.Main.main(Main.java:37)
Practically, the getter getPower()
returned null
since the power
field is null
. Why? The answer is in the line return new ChainSaw(model, null, (int) (Math.random() * 100));
of the initChainSaw()
method. Because we didn’t decide yet on the algorithm for calculating the power of a chainsaw, we passed null
to the ChainSaw
constructor. Further, the constructor simply sets the power
field as this.power = power
. If it was a public constructor, then most probably we would have added some guarded conditions, but being a private constructor, it is better to fix the issue right from the root and not pass that null
. Since the power
is a String
, we can simply pass an empty string or a suggestive string such as UNKNOWN_POWER
. We also may leave a TODO comment in code such as // TODO (JIRA ####): replace UNKNOWN_POWER with code
. This will remind us to fix this in the next release. Meanwhile, the code has eliminated the NPE risk.
Okay, after we fixed all these five NPE risks, the code has become the following (the added code is highlighted):
public final class ChainSaw {
private static final String UNKNOWN_POWER = "UNKNOWN";
private static final List<String> MODELS
= List.of("T300", "T450", "T700", "T800", "T900");
private final String model;
private final String power;
private final int speed;
public boolean started;
private ChainSaw(String model, String power, int speed) {
this.model = model;
this.power = power;
this.speed = speed;
}
public static ChainSaw initChainSaw(String model) {
if (model == null || model.isBlank()) {
throw new IllegalArgumentException("The given model
cannot be null/empty");
}
for (String m : MODELS) {
if (model.endsWith(m)) {
// TO DO (JIRA ####): replace UNKNOWN_POWER with code
return new ChainSaw(model, UNKNOWN_POWER,
(int) (Math.random() * 100));
}
}
throw new UnsupportedOperationException(
"Model " + model + " is not supported");
}
public int performance(ChainSaw[] css) {
if (css == null) {
throw new IllegalArgumentException(
"The given models cannot be null");
}
int score = 0;
for (ChainSaw cs : css) {
if (cs != null) {
score += Integer.compare(this.speed, cs.speed);
}
}
return score;
}
public void start() {
if (!started) {
System.out.println("Started ...");
started = true;
}
}
public void stop() {
if (started) {
System.out.println("Stopped ...");
started = false;
}
}
public String getPower() {
return power;
}
@Override
public String toString() {
return "ChainSaw{" + "model=" + model
+ ", speed=" + speed + ", started=" + started + '}';
}
}
Done! Now, our code is NPE-free. At least until reality contradicts us and a new NPE occurs.