Reader small image

You're reading from  Java Coding Problems - Second Edition

Product typeBook
Published inMar 2024
PublisherPackt
ISBN-139781837633944
Edition2nd Edition
Right arrow
Author (1)
Anghel Leonard
Anghel Leonard
author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard

Right arrow

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.

Previous PageNext Page
You have been reading a chapter from
Java Coding Problems - Second Edition
Published in: Mar 2024Publisher: PacktISBN-13: 9781837633944
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Anghel Leonard

Anghel Leonard is a Chief Technology Strategist and independent consultant with 20+ years of experience in the Java ecosystem. In daily work, he is focused on architecting and developing Java distributed applications that empower robust architectures, clean code, and high-performance. Also passionate about coaching, mentoring and technical leadership. He is the author of several books, videos and dozens of articles related to Java technologies.
Read more about Anghel Leonard