Book Image

Java Coding Problems - Second Edition

By : Anghel Leonard
Book Image

Java Coding Problems - Second Edition

By: Anghel Leonard

Overview of this book

The super-fast evolution of the JDK between versions 12 and 21 has made the learning curve of modern Java steeper, and increased the time needed to learn it. This book will make your learning journey quicker and increase your willingness to try Java’s new features by explaining the correct practices and decisions related to complexity, performance, readability, and more. Java Coding Problems takes you through Java’s latest features but doesn’t always advocate the use of new solutions — instead, it focuses on revealing the trade-offs involved in deciding what the best solution is for a certain problem. There are more than two hundred brand new and carefully selected problems in this second edition, chosen to highlight and cover the core everyday challenges of a Java programmer. Apart from providing a comprehensive compendium of problem solutions based on real-world examples, this book will also give you the confidence to answer questions relating to matching particular streams and methods to various problems. By the end of this book you will have gained a strong understanding of Java’s new features and have the confidence to develop and choose the right solutions to your problems.
Table of Contents (16 chapters)
1
Text Blocks, Locales, Numbers, and Math
Free Chapter
2
Objects, Immutability, Switch Expressions, and Pattern Matching
14
Other Books You May Enjoy
15
Index

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.