Skip to content

Instantly share code, notes, and snippets.

@edreyer
Last active April 16, 2024 13:24
Show Gist options
  • Save edreyer/92616b9172c7f827393a0015fd2b1641 to your computer and use it in GitHub Desktop.
Save edreyer/92616b9172c7f827393a0015fd2b1641 to your computer and use it in GitHub Desktop.
null handling in both Java and Kotlin

Null handing in both Java and Kotlin

Most Java engineers know how to use java.util.Optional for handling singular null fields. Things get a little more interesting when you have to combine two (or more) Optionals into a new Optional instance.

Below, we look at how you might do this in both Java and Kotlin. We will use a basic domain object (Person) and how we might construct it using a factory method that takes as arguments the individual nullable properties that make up a Person instance.

Java version

We illustrate two ways to create a Person. The first uses an imperative "traditional" style of Java.

This method is unsafe in that isPresent() checks are separate from get() calls. In real code, it's possible that at some point in the future, maybe due to a refactoring, the isPresent() check gets removed leaving you with a naked get() call and possible runtime NPE. This is what makes this approach risky.

The second uses a more declarative functional style. This version is much more concise, and when the mechanism is understood presents a far lower cognitive load. However, it requires an understanding of flatMap() versus map() which can take a while to grok for many (most?) engineers. Until that understanding is acquired, it could actually lead to a higher cognitive load. This approach is functionally equivalent to the imperative approach, but with the following advantages:

  • No need for separate isPresent() and get() calls. In fact, no need at all for either of those.
  • We can safely operate on possible null values without ever having to worry about whether or not they are null. This is the real value of using Optional.
  • Much less code
  • Much less cognitive load (when the mechanism is understood)

Kotlin version

We do a similar thing here here, where we present two approaches. First a vanilla kotlin example, then a more sophisticaed but powerful DSL-driven approach. Even using the vanilla kotlin approach, it's an improvement over Java's functional approach, though both are similar. In this case, there's no need to reason about whether to use flatMap() or map() functions. Instead, we use the let() scope function, which can be used at all nested levels.

The 2nd example uses a Kotlin specific feature. That being, the ability to create DSLs.

In this example, we are simulating scala's "for comprehensions" (google it). We are essentially creating a "comprehension" that allows us to create a null-safe person creator in a single line, with no nested lambdas, or nesting of any kind.

This approach is functionally equivalent, but a lot easier to use. It just requires a bit of code to setup the DSL, which can be reused anywhere in your project.

This approach to create a "comprehension" DSL can be used for any number of use cases. In this case, we focus on handling nullability.

static class Person {
public String name;
public Integer age;
public String email;
public Person(String name, Integer age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// imperative approach
public static Optional<Person> createPerson(String name, Integer age, String email) {
var nameO = Optional.ofNullable(name);
var ageO = Optional.ofNullable(age);
var emailO = Optional.ofNullable(email);
if (nameO.isPresent() && ageO.isPresent() && emailO.isPresent()) {
return Optional.of(new Person(nameO.get(), ageO.get(), emailO.get()));
} else {
return Optional.empty();
}
}
// declarative approach (formatted for readability)
public static Optional<Person> createPerson2(String name, Integer age, String email) {
return Optional.ofNullable(name).flatMap(n ->
Optional.ofNullable(age).flatMap(a ->
Optional.ofNullable(email).map(e ->
new Person(n, a, e)
)));
}
}
// our data model
data class Person(val name: String, val age: Int, val email: String)
// vanilla Kotlin
fun createPerson(name: String?, age: Int?, email: String?): Person? =
name?.let { n ->
age?.let { a ->
email?.let{ e ->
Person(n, a, e);
}
}
}
// Using the nullable monad comprehension DSL, eliminates nesting
fun createPerson(name: String?, age: Int?, email: String?): Person? =
nullable { Person(name.bind(), age.bind(), email.bind()) }
// --------------
// START: DSL to create "monad comprehension" to support nullable types
// --------------
// custom exception used to filter to just this condition
private class NullableException: Exception()
// When in scope, the "bind()" method is accessible to any type.
class NullableScope {
fun <T> T?.bind(): T = this ?: throw NullableException()
}
// DSL using function receiver to provide the NullableScope
// More on function receivers: https://www.kotlinprimer.com/extension-functions/basics/functions-with-receiver/
inline fun <T> nullable(block: NullableScope.() -> T): T? = with(NullableScope()) {
try {
block() // "bind()" method available to code in this block
} catch (e: NullableException) {
null
}
}
// --------------
// END: DSL to create "monad comprehension" to support nullable types
// --------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment