How to Banish NullPointerException from Your Java Code

If you‘re a Java developer, you‘ve likely encountered the dreaded NullPointerException (NPE) at some point. It‘s the most common exception type in Java according to research by OverOps, an error monitoring service. In this article, we‘ll dive deep into what causes NPEs and learn proven techniques to prevent them from plaguing your code. By the end, you‘ll have the knowledge and tools to make NullPointerException a thing of the past.

Understanding NullPointerException

So what exactly is a NullPointerException? Put simply, it‘s a runtime exception that occurs when you try to use a reference that points to no object, i.e. null. For example:

String s = null;
s.length();  // Throws NullPointerException

Here we‘re declaring a String variable s and explicitly assigning it to null. Then when we try to call the length() method on s, we get an NPE because s does not refer to a String object.

While the above example is trivial, NPEs are often not so obvious. They frequently occur deep in the bowels of a program far removed from the actual bug. Maybe an object returned from some method is unexpectedly null, and that null gets passed around and used somewhere later. This indirection can make NPEs hard to track down.

So where do all these nulls come from anyway? Tony Hoare, the inventor of null references, famously called them his "billion-dollar mistake". Null references seemed like a good idea at the time, but they can make code more error-prone by introducing uncertainty about whether a variable is null or not.

Some common sources of null:

  • Uninitialized variables and fields (in Java they default to null)
  • Explicitly assigning null to indicate no value
  • Returning null from methods to represent failure or absence of a result

Defensive Programming

The first line of defense against NPEs is defensive programming. This means writing code that checks for null and handles it gracefully. For example:

public void doSomething(String s) {
  if (s != null) {
    System.out.println(s.length()); 
  } else {
    System.out.println("String was null");
  }
}

By checking if s is null before using it, we avoid the NPE. The downside is that null checks can quickly litter your code making it more verbose and harder to read.

Often I see null checks like this:

if (result != null && result.getValue() != null) {
  System.out.println(result.getValue());
} 

Multiple null checks in a row are a code smell that the design has too many nullable references flying around. We‘ll see some cleaner alternatives shortly.

Fail Fast

Another approach is to fail fast when encountering an unexpected null. Instead of allowing a null to propagate and potentially cause an error later, we can check parameters upfront and throw an exception if they‘re null:

public void doSomething(String s) {
  if (s == null) {
    throw new IllegalArgumentException("s must not be null");
  }
  // Do something with s
}

Now the NPE is avoided and we get a more descriptive error message at the source of the problem. The Objects.requireNonNull utility method in Java 7+ is handy for this:

public void doSomething(String s) {
  Objects.requireNonNull(s, "s must not be null");
  // Do something with s  
}

This comes with the overhead of an extra null check on every method call. Some IDEs and static analyzers can help by inferring nullness annotations and warning you if a null may be passed to a method expecting a non-null value.

Optional Type

Java 8 introduced the Optional type as a way to represent values that may or may not be present. Instead of returning null, you return an Optional that either contains a value or is empty. For example:

public Optional<Customer> findCustomerById(long id) {
  // ...
  return Optional.ofNullable(customer);
}

Now the client code can explicitly check if a value is present using isPresent() or get() the value if it exists. Optional also supports functional-style operations like map, filter, and ifPresent to transform and use the value if present.

Optional<Customer> customerOpt = findCustomerById(123);
if (customerOpt.isPresent()) {
  System.out.println(customerOpt.get().getName());
} 

customerOpt.ifPresent(c -> System.out.println(c.getName()));

The advantage is that Optional makes it explicit in the API when a value may be absent. The client is forced to deal with the case of no value instead of unexpectedly getting null. It‘s still possible to misuse Optional by calling get() without checking isPresent() but it‘s a big improvement over nullable references.

Immutability

One of the best ways to prevent NPEs is to make your objects immutable wherever possible. An immutable object‘s state cannot be changed after construction. This means all its fields must be final and set in the constructor. With immutability, you can be sure an object is fully and properly initialized. For example:

public final class Money { 
  private final double amount;
  private final Currency currency;

  public Money(double amount, Currency currency) {
    this.amount = amount;
    this.currency = Objects.requireNonNull(currency);
  }

  // Getters but no setters
}

Once a Money instance is created, we know amount and currency are always non-null. The requireNonNull check in the constructor ensures currency can never be null.

Immutability has many other benefits beyond avoiding nulls like thread-safety and ease of reasoning. Aim to make your value objects immutable by default.

Factory Methods

When you need to return different types of objects based on some condition, factory methods are preferable to returning null. For example, instead of:

public Animal getAnimal(String type) {
  if (type.equals("dog")) {
    return new Dog();
  } else if (type.equals("cat")) {
    return new Cat();  
  }
  return null;
}

We can have separate factory methods for each case:

public Animal createDog() {
  return new Dog();
}

public Animal createCat() { 
  return new Cat();
}

Or use an enum to represent the different types:

public enum AnimalType {
  DOG {
    public Animal create() {
      return new Dog(); 
    }
  },

  CAT {
    public Animal create() {
      return new Cat();
    }
  };

  public abstract Animal create();
}

Now the client chooses the appropriate enum value and invokes create() on it, avoiding any nulls.

Avoid Nulls in Collections

If you have a collection (array, list, set, map etc.), try to keep its elements non-null. Adding nulls to a collection is a recipe for NPEs when iterating or streaming over it.

If you need to represent missing values in a collection, consider using Optional or some other special value object. For example, Google Guava provides an Absent class for this purpose.

When getting a value from a map, use getOrDefault to provide a default value instead of null.

Map<String, String> map = ...;
String value = map.getOrDefault(key, "");

For primitive collections, use the boxed types like Integer or Long which can be null. Avoid autoboxing of primitives like int or long which can cause NPEs.

Static Analysis

Modern IDEs like IntelliJ IDEA and Eclipse have sophisticated static analyzers that can detect potential null pointer dereferences. They can infer annotations like @Nullable and @NotNull and highlight cases where a variable may be null.

For example, IntelliJ warns you if you don‘t check the result of a method annotated with @Nullable before dereferencing it:

IntelliJ Nullable Warning

FindBugs is another popular static analysis tool that can detect impossible null pointer dereferences.

While not foolproof, static analysis can catch many potential NPEs before they happen. Make sure you enable these warnings in your IDE and pay attention to them.

Null Safety in Other Languages

While we‘ve focused on Java, many other languages have taken innovative approaches to null safety.

Kotlin aims to eliminate NPEs through its type system. Types are non-nullable by default and can only be made nullable by adding a ? suffix. The compiler tracks nullability and ensures you check for null before dereferencing a nullable type. This makes NPEs virtually impossible in Kotlin code.

C# 8 introduced nullable reference types which bring similar null safety to C#. The compiler warns you if you dereference a potentially null reference without checking it first.

Rust, Haskell and ML family languages generally don‘t have null at all. Instead they use constructs like Rust‘s Option to represent values that may not exist.

These approaches put the burden on the programmer to explicitly deal with nulls rather than relying on runtime exceptions.

Conclusion

NullPointerException may never be eliminated entirely, but by using the techniques outlined in this article you can dramatically reduce their occurrence:

  • Use null sparingly and avoid returning or passing it in APIs
  • Validate public method parameters and fail fast
  • Implement default or special case behavior instead of returning nulls
  • Use Optional for values that may not be present
  • Favor immutable objects that are always fully initialized
  • Enable IDE and static analysis warnings for potential null dereferences

Remember, every time you type null, you should think carefully if there‘s a cleaner alternative that expresses your intent more clearly. Removing unnecessary nulls will make your code more robust, easier to reason about, and less prone to those pesky NullPointerExceptions.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *