Saturday, July 31, 2021

Designing NullPointerException Safe APIs in Java

I strongly believe that programming is a skill we never truly master, we get better at it with experience but every project brings new lessons and sometimes can be humbling. One of the things that we can do to get better at this skill is to learn more programming languages. Surely, we need to master one or few languages which are our daily bread earners but restricting ourselves to just those is not healthy. It expands and opens up our minds to different ideas and concepts. That being said, I am guilty of not doing this myself. I've restricted myself to a family of programming languages which essentially look identical (C/C++/C#/Java et al). 

In the recent past I've started to look at other languages more seriously. Learning new programming languages is a bit difficult for me personally because I get bored very quickly in learning how to define a function, or how to write a conditional statement in a new language. But once these hurdles are crossed, then it's a more fruitful experience. You start understanding the philosophy of the language, why was it designed the way it is designed, what are the new ideas which are not available in another language and so on. 

One of the languages that I've found very interesting recently is Rust, I am always looking for things to do in Rust. It is a language which is designed based on some innovative ideas but at the same time is pragmatic. For a full list of Rust's innovative features, I refer to this ACM article. One of its features which probably gets lost in the list of all the other shinier features is that it does not have the problem of NullPointerException which is prevalent in many of the mainstream languages.

Tony Hoare (famous for inventing QuickSort) is credited for introducing the concept of null in programming languages. He introduced the null type in ALGOL and many years later he famously apologised for having done that, he said it was a billion dollar mistake. Many programming languages aped the concept of null pointer, which has been a cause numerous runtime bugs.

Rust avoids NullPointerException by not providing the option of returning the null type to the programmer. The equivalent of not returning any valid result in Rust is called the None type. But the compiler forbids you from returning a None value directly. When writing a function in Rust, you either define the function to return a value of a specific type, and then the function has to return a value of that type. If the function does not need to return anything, that's also possible. But if you are writing a function which may have a value to return sometimes and may not have any valid result to return other times you need to define your function to return the Option<T> type. 

Sidenote: There is another way of getting a null pointer error, which is when we dereference a pointer pointing to an uninitialized memory or to an object which does not exist anymore. Rust avoids those as well but I'm only talking about the problem of null values here.

Following is an example of a Rust function which can return a value only in some cases. We have a struct to represent a matrix and a function to find the inverse of the matrix. But we know that sometimes the matrices are non-invertible, specifically when their determinant is 0.

pub struct Matrix {
nrows: usize,
ncols: usize,
vals: Vec<f32>
}

impl Matrix {
pub fn inverse(&self) -> Option<Self> {
let det = self.det();
if det == 0.0 {
return None;
}
let mut inverse_mat = Self::new(self.nrows, self.ncols);
for i in 0..self.nrows {
for j in 0..self.ncols {
let c = self.cofactor(i, j);
inverse_mat.set(j, i, c / det);
}
}
return Some(inverse_mat);
}

}

Here we see that the function is defined to return an Option<Self> type. Which basically declares that the function may return an object of type Matrix or it may return None. Option is an enum defined in Rust, which consists of two possible values: None and Some<T>. 

pub enum Option<T> {
None,
Some(T),
}

When a function is declared to return an Option type value, whoever calls that function has to make sure to check whether they got a None value return or an actual value. Here is an example how such a function may be used.

fn test_inverse2() {
let a = Matrix::from_array(4, 4, &[8.0, -5.0, 9.0, 2.0,
7.0, 5.0, 6.0, 1.0,
-6.0, 0.0, 9.0, 6.0,
-3.0, 0.0, -9.0, -4.0]).unwrap();
let inverse_result = a.inverse();
match inverse_result {
Some(inverse) => println!("Inverse result: {:?}", inverse),
None => println!("Non invertible matrix")
}
}

After getting the result of the inverse function, we are using pattern matching to check the actual value received and taking action accordingly. In this way we are guaranteed that the caller of inverse will take care of handling the case when inverse doesn't return any value. 

In other languages such as Java, which allow returning null directly have the problem of NullPointerException, because most of the times we don't take care of not checking for a null value being returned. And sometimes we are too careful and litter the code with null checks all over the place even when there is a good chance that there is no probability of getting a null value in many of the places.  For example:

String managerName = bob.department.manager.name;

There is a good chance of getting an NPE in this chain. To avoid that we may have to write this monstrosity:

if(bob != null) {
Department department = bob.department;
if(department != null) {
Employee manager = department.manager;
if(manager != null) {
String name = manager.name;
if(name != null) {
//do something
}
}
}
}

If the language provides us with a way to declare when a function can return a null value, not only can we prevent the pest of NullPointerExceptions but we can also write clearer code. Java doesn't have any option of preventing NullPointerException at the language level but as part of Java 8 they have added the Optional<T> type in java.utils package. It seems similar to Rust's Option type. Basically we can write methods declared to return an Optional<T> type, so the caller knows that they have to check whether the function returned some value or not. It is still possible to write functions which return null so NPEs are not completely gone but within a team we can agree upon designing APIs which declare to return Optional<T> type whenever they think there is a possibility of returning null from the function and if a function is declared to return something else we can assume (in good faith) that the function is guaranteed to return a proper result.

class Scratch {
public static Optional<Long> divide(long numerator, long denominator) {
if (denominator == 0) {
return Optional.empty();
}
return Optional.of(numerator / denominator);
}

public static void main(String[] args) {
Optional<Long> result = divide(100, 0);
Long quotient = result.orElseThrow(() ->
new IllegalArgumentException("Divide by 0"));
System.out.println("Result: " + quotient);
}
}

I am not sure if Java is ever going to go as far as to disallow returning null from methods but this comes close to getting rid of those pesky NPEs.

Optionals have a great deal of features in built to avoid writing verbose checks, one of which I've used above where if the result contains a value I will get the value or I can throw an exception of my choice. Similarly there are other options available such as getting a default value back if the Optional does not contain any value.


No comments:

Post a Comment