Avoiding NullPointerException in Java6 min read
NullPointerException
is one of the most hideous exceptions in Java. It can silently stop the execution of one thread, or it can, fatally terminate the execution of the whole program. As a Java programmer, you probably have to deal with it many times during your career. Seemingly we cannot eliminate the NullPointerExcetpion
entirely since it’s a part of the language. However, we still can write a null-safe program by applying some of the best practices.
What is NullPointerException?
Before diving into the treatment, let’s quickly look at the cause. NullPointerException
happens when you try to dereference an object that hasn’t been initialized. In Java, everything is an object except some primitive types, then it’s trivial to make a program that throws NullPointerException
simply like this:
String s;
s.toUpperCase(); // NullPointerException
Integer num = null;
num.toString(); // NullPointerException
OK, that’s enough. Let’s find out what we can do to limit the occurrence of situations like the above.
Return default value instead of returning null
One of the most frequent strategies I’ve been applying to avoid NullPointerExcetpion
is the usage of the default value. Here are some simple rules for returning default values:
- Return
""
(empty string) as the default value if a string is needed. - Return
-1
(or any other distinguishable value) as the default value for wrapper primitive types ofInteger, Long, Double, Float, Byte
, returnfalse
forBoolean
and'\0'
forCharacter
. - For collections, return an empty collection (e.g.
Collections.emptyList(), List.of()
, etc…) instead of a null. - In the case of a complex object, create a defensive object and return it when cannot find any possible value.
By returning these default values, it’s less likely someone calls your method and gets a NullPointerException
, it’s obvious in the case of strings and other primitive wrappers. However, when a method returns a complex object, here is an example:
@AllArgsConstructor
class Address {
public static final Address NONE = Address.create(-1L, -1L, "");
private final double lat;
private final double lon;
private final String addressName;
public static Address create(double lat, double lon, String addressName) {
return new Address(lat, lon, addressName);
}
}
class AddressService {
private final Map<String, Address> locationToAddress = new HashMap<>();
public Address get(String lid) {
Address address = locationToAddress.get(uid);
if (address == null) {
return Address.NONE;
}
return address;
}
}
In the example above, we return an Address
from the given lid
, in case we cannot find any address, then we return Address.NONE
. Our address object is pretty straightforward, we create the NONE
default object by applying some of our existing rules.
When there is a caller using the get
method in the AddressService
class, they only have to check the address against the NONE
value:
Address address = addressService.get(...);
if (Address.NONE == address) {
// address is not present
} else {
address.getAddressName(); // will not throw null pointer
}
If in case the object is nested by different layers, at that point, we can recursively assign default values to them:
@AllArgsConstructor
class LocationAddress {
public static final LocationAddress NONE = LocationAddress.create("", Address.NONE);
private final String locationId;
private final Address address;
public static LocationAddress create(String lid, Address address) {
return new LocationAddress(lid, address);
}
}
The same rule for default value can be applied to enums as well:
@AllArgsConstructor
public enum Category {
COMPUTER_SOFTWARE("computer_software"),
PERSONAL_BLOGGING("personal_blogging"),
CONSULTING("consulting"),
NONE("none"); // default enum value
public final String category;
}
Check for preconditions
Next, we examine our methods having some arguments passed from the outside, the sooner we catch the error, the less catastrophic the outcome might be, especially for deep methods that have been through multiple layers of abstractions. When a certain precondition is not met, it’s better to stop right there and notify the caller, for example:
public static String parseResource(File file) throws IOException {
Preconditions.checkNotNull(file);
try (BufferedReader fileReader = new BufferedReader(new FileReader(file))) {
StringBuilder sb = new StringBuilder();
String s;
while (!(s = fileReader.readLine()).isBlank()) {
sb.append(s);
}
return sb.toString();
}
}
Here we try to get the content from a file, and we eagerly check for preconditions by using the utility class of the Guava project, if it’s null we immediately throw an exception to the caller, otherwise, we can continue to process our next step.
Document Nullable and NotNull for public APIs
Let’s imagine you create a utility class that has been used by many other developers, it’s crucial to have some documents for each of the public methods so that others can use your code correctly. Some of the criteria we should question when documenting public APIs:
- What does this method do?
- What is the input? Can the input be null?
- What is the output of the method, is this nullable?
- Does the method throw any exception when the state is invalid?
For example, we have a method maybeAlert
which potentially sends an alert to some channels (Mail, Slack, etc…) when a certain condition is met:
/**
* Sending an alert to the destination channel when the certain threshold is met.
*
* @param potentialAlert contains the alert state of the calling service
* @return true - if the alert is sent, false otherwise.
*/
@Override
public boolean maybeAlert(@NotNull PotentialAlert potentialAlert) {
if (PotentialAlert.NONE != potentialAlert) {
boolean alerted = potentialAlert.isAlerted();
this.alertCreatorService.put(potentialAlert);
synchronized (lock) {
if (!alerted) {
boolean reachedThreshold = this.alertCreatorService.reachAlertThreshold(potentialAlert);
if (reachedThreshold) {
return sendAlert(potentialAlert);
}
} else {
log.info("Already alerted, alert state: {}", potentialAlert);
}
}
}
return false;
}
Annotations in Java can be explicitly for documenting our code, in this case, we use the @NotNull
to indicate that the PotentialAlert
should not be null, and once the condition is met, we can fully focus on the business matter. Some modern IDEs will give you warnings if you pass a null value to the parameter annotated with @NotNull
. In contrast, we can also annotate an object that’s nullable with the @Nullable
.
That’s for the public APIs, but the methods can only be used inside a class (e.g. private methods) we should validate the conditions ourselves, make sure NullPointerExcetpion
cannot be thrown, and they don’t necessarily have excessive documents.
Using Optional as a wrapper
Java 8 has introduced multiple cool classes, one of them being the Optional
class, it’s the wrapper object that either wraps something or nothing at all. With any public APIs that potentially return a null
value, the Optional class seems a good fit for this purpose:
public Optional<Discount> getDiscount(Order order) {
if (order.calculateDiscount() <= 0) {
return Optional.empty();
}
return Optional.of(order.getDiscount());
}
Any time we want to use the getDiscount
method, the semantics of the Optional is that it can hold an empty object. The caller can then rely on this fact to do some logic like:
Optional<Discount> maybeDiscount = getDiscount(...);
maybeDiscount.ifPresentOrElse(
discount -> {
customer.saveDiscount(discount);
},
() -> LOGGER.info("No discount present!"));
Be a skeptic
In many cases, we have to call untrusted code, and if the documentation for it is not available or not well-documented, or a complicated method you cannot know what will be its return value. It’s your responsibility to at least check for the state of the given object, in the end, it’s better safe than sorry. For example, deference the invoice object can potentially throw an exception:
public double calculateExpense(List<Integer> invoiceIds) {
double totalExpense = 0;
for(final var id : invoiceIds) {
Invoice invoice = invoiceService.getInvoice(id);
if (invoice != null && invoice.getAmount() > 0) { // here check for preconditions
totalExpense += invoice.getAmount();
}
}
return totalExpense;
}