Skip to main content

Null Safety

  • Related classes: Option<T>, Result<T>, UnsafeUnwrapException
  • Related annotations: @NonNull, @Nullable, @NullMarked, @NullUnmarked

The problem​

It's not clear when a given variable may be null or not based on it's type. This either forces you to assume every variable is null, and add a null check, or, more commonly, forget to check for null, and get a nasty NullPointerException at runtime, and now you need to debug where in your entire code it arrived from.

The solution (kinda)​

What if, along with the type of the variable, we specify if it can potentially be null. Kapi bundles the Jspecify library, which allows you to annotate your types:

  • @NonNull String - a string that cannot be null.
  • @Nullable String - a string that may or may not be null.

The entirety of Kapi uses these annotations to clearly indicate when you absolutely need a null check, and when you don't need a null check.

tip

You can use Jspecify to annotate your own code too!
To get started, check out Jspecify's User Guide.

A Better Solution​

Sure, annotations can help, but they are just a hint that static analysis tools can use to warn you.
We need a solution that stops you from accidentally forgetting a null check and getting a NullPointerException at runtime.

info

You might've encountered the Option<T> and Result<T, E> classes.
They are usually returned by Kapi's methods.

Option​

Inspired by Rust's Option and Java's Optional, Kapi provides an Option<T> type.

An option, can be in one of 2 states:

  • Some(value) - contains a value, that cannot be null.
  • None - doesn't contain a value, (similar to a null value).

To get the value inside, you must explicitly handle both cases:

Option<Integer> opt = ...;
opt.match(value -> {
// We can now use the value safely
}, () -> {
// There is no value, handle the error appropriately
});

This now prevents you from accidentally forgetting a check, but it's still pretty tedious to work with.

The Option type has a bunch of methods to make working with it nicer, here are some notable ones:

  • map(value -> { /* code */ }) - maps an option of a given type to a different type.
  • unwrapOr(defaultValue) - returns the value if it exists, or the provided default value if it doesn't exist.
  • inspect(value -> { /* code */}) - runs the code if the value exists or does nothing if it doesn't exist.
  • expect(message) - returns the value if it exists, or throws a UnsafeUnwrapException with the given message

Result​

Kapi provides a Result<T,E> type (inspired by Rust).

Similar to an option a result can be in one of 2 states:

  • Ok(value) - the "desired" value.
  • Err(value) - a value indicating an error.

Like Option, you can match on a result to handle both cases:

Result<String, Exception> result = ...;
result.match(ok -> {
// Can use the ok value as needed
}, err -> {
// Can handle the error value, such as logging it.
});

Most of Option's methods also exist in result, but here are some result-specific ones:

  • ok() - convert the result to an option, discarding the error.
  • inspectErr(err -> { /* code */ }) - similar to inspect but used on the error value