Press "Enter" to skip to content

Kotlin’s java.util.Optional API Equivalents

Kotlin truly shines when it comes to avoiding excessive bureaucracy of classical Type-Driven approaches to optionality.

Let’s see how does its native approach to null-safety compare to java.util.Optional.

1. Recap of Null-Safety in Kotlin

If you’re familiar with Kotlin’s approach to null-safety, feel free to skip this section with no regrets.

Kotlin implemented its approach to null-safety that can leverage extra compiler support. Simply put, each type is non-nullable by default but can be made nullable by appending the question mark to the type name.

So, that’s non-nullable String:

val foo : String = "42"

and this one is nullable:

val foo2 : String? = "42"

As simple as it is. The striking thing happens if we try to call a method/field on the second one – the code doesn’t compile:

foo2.length 

// Error:(19, 9) Kotlin: Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

but if we perform a null-check first, we get a green light from the compiler:

if (foo2 != null) {
    foo2.length
}

This is just a short recap – more about this can be found in the official documentation.

2. Optional API Equivalents

2.1. of()/ofNullable()

Whenever we want to instantiate an Optional in Java, we need to use dedicated static factory methods like:

Optional<String> foo1 = Optional.ofNullable(null);
Optional<String> foo2 = Optional.of("...");

Since optionality is Kotlin is inbuilt into the type system, there are no dedicated static factory methods or any other special mechanisms for wrapping our classes.

Instead, we can simply assign our values directly to our nullable types:

val foo1 : String? = null
val foo1 : String? = "..."

2.2. get()

The controversial get() method allows us to literally tear out values carefully encapsulated inside Optional instances – if a given Optional was empty, a NoSuchElementException gets thrown – which practically makes the get() method a syntactic sugar replacing an NPE with NoSuchElementException.

In most cases shouldn’t be used unless you know what you are doing – there’s even an initiative to deprecate the method.

In Kotlin, same can be achieved using non-null asserted calls “!!”:

val foo: String? = null
foo!!.length

2.3. map()

The map() method is the primary Optional API that’s used for performing computations on values residing inside an Optional instance.

For example:

Optional<Integer> foo1 = Optional.ofNullable(42);
Optional<String> foo2 = foo1.map(v -> v.toString);

In Kotlin, we can use the let() method to achieve similar functionality. We just need to remember about using safe-calls (?.):

val foo1 : Int? = 42
val foo2 : String? = foo1?.let { v -> v.toString() } // or simply { it.toString() }

The interesting part is that in such simple scenarios we can simply replace the let() call with a direct safe-call to toString() which allows reducing the amount of boilerplate even further and fully leveraging the first-class-citizenship of null-safety in Kotlin:

val foo2 : String? = foo1?.toString()

2.4. flatMap()

The flatMap() is the map()’s alternative dedicated to avoiding nesting Optionals:

Optional<Integer> foo1 = Optional.ofNullable(42);
Optional<Optional<String>> foo2 = foo1.map(v -> Optional.ofNullable(v));

We can avoid this using the flatMap() instead of the map():

Optional<String> foo2 = foo1.flatMap(v -> Optional.ofNullable(v));

In Kotlin, however, there’s no possibility of ending up with objects having multiple dimensions of nullability.

We can append multiple question marks to each type, but at the end of the day, they will be treated like a single one:

val foo1 : Int??? = 42

Even if we deliberately decide to return a nullable object from a let() call, the result simply has one degree of nullability:

val foo1 : Int? = 42
val foo2 : String? = foo1?.let {
    val foo2: String? = null
    foo2
}

That simplifies some operations considerably but takes away the ability to play with multiple degrees of mutability – which isn’t such a painful trade-off.

2.5. filter()

In order to achieve the filter() functionality, we can simply leverage the takeIf() method instead:

val foo: String? = "42"
val filteredFoo: String? = foo
  ?.takeIf { it.isNotEmpty() }

2.6. orElse()/orElseGet()/orElseThrow()

All those are “terminal” Optional methods allowing us to handle the sad-path by providing a fallback value or simply by throwing an exception.

All of them can be replaced using a single “?:” operator:

foo ?: "42"
foo ?: someExpensiveCall("default")
foo ?: throw NoSuchElementException()

Yet, it’s important to underline that the value on the right-hand side gets always computed lazily allowing us to avoid performing redundant operations.

3. A Real-life Example

Summarizing everything covered so far, a hypothetical idiomatic example could look like:

val x: Int? = 7                 // ofNullable()

val result = x
  ?.let { ... }                 // map()
  ?.takeIf { ... }              // filter()
  ?: 42                         // orElseGet()

4. Conclusion

Although I appreciate the effort of making some abstract Functional Programming ideas a bit less abstract regarding naming conventions (for example, takeIf() instead of filter()), I’d still feel more comfortable working with standard maps and flatmaps used by the rest of the functional world.

The write-up was inspired by this StackOverflow question.




If you enjoyed the content, consider supporting the site: