Author profile picture

Java Optionals and Kotlin Nulls

When Java 8 introduced Streams for operating on collections of data, it also introduced a similar concept, Optional, which has many methods that are similar to Stream, but operates on a single value that might or might not be present.

As you migrate your projects from Java to Kotlin, you might come across some Optional objects. What should you do? Should you leave them as Optional, or change them to more idiomatic Kotlin?

In this guide, we’ll take a look at how Kotlin’s null-safety features can be used to replace Java 8’s Optional class. Then we’ll wrap up with a few more thoughts about the trade-offs between using Optional and using Kotlin with nullable types.

To explore all of the possibilities, we’ll assume these constants:

static Optional<String> present = Optional.of("Hello");
static Optional<String> absent = Optional.empty();
val present: String? = "Hello"
val absent: String? = null

Creation methods

empty()

The Kotlin equivalent of assigning an empty() is to assign a null.

Optional<String> s1 = Optional.empty();
val s1: String? = null

of()

The of() method creates an Optional if a value is present, or forces an immediate NullPointerException otherwise. In Kotlin, we have to go out of our way to throw the exception.

Optional<String> s2 = Optional.of("Hello");
Optional<String> s3 = Optional.of(null);

val s2: String? = "Hello"
val s3: String? = null
	 ?: throw NullPointerException()

ofNullable()

The ofNullable() method works the same as of(), except that instead of throwing a NullPointerException, a null value produces an empty. The Kotlin equivalent is straightforward.

Optional<String> s4 = Optional.ofNullable("Hello");
Optional<String> s5 = Optional.ofNullable(null);
val s4: String? = "Hello"
val s5: String? = null

Transformation methods

map()

The map() method allows you to transform an Optional to an Optional of another value. The equivalent in Kotlin is let(). Since our example uses a nullable String, be careful not to accidentally use the extension function CharSequence.map(), which will map each character of the String instead of the String itself.

present.map(it -> "The value is " + it);
// Optional[The value is Hello]

present.map(it -> null);
// Optional.empty

absent.map(it -> "The value is " + it);
// Optional.empty

absent.map(it -> null);
// Optional.empty
present?.let { "The value is $it" }
// The value is Hello

present?.let { null }
// null

absent?.let { "The value is $it" }
// null

absent?.let { null }
// null

In some cases, it’s not necessary to call let() at all. This happens when you’re mapping to a property or function on the nullable variable. For example:

present.map(String::length);
// Optional[5]

present.map(it -> it.toUpperCase());
// HELLO
present?.length
// 5

present?.toUpperCase()
// HELLO

flatMap()

When mapping an Optional in Java, sometimes you have to unwrap another Optional. To do this, you use flatMap() instead of map(). With Kotlin’s null system, the value is either present, or null, so there’s nothing to unwrap. This means that Kotlin’s equivalent for flatMap() and map() are similar.

Notice that you have to go out of your way to throw a NullPointerException in Kotlin - otherwise, the third example below would have just returned a null.

(For this example, we’ll introduce a third optional value, other).

Optional<String> other = Optional.of("World");

present.flatMap(it -> other);
// Optional[World]

present.flatMap(it -> absent);
// Optional.empty

present.flatMap(it -> null);

// throws NullPointerException

absent.flatMap(it -> other);
// Optional.empty

absent.flatMap(it -> absent);
// Optional.empty

absent.flatMap(it -> null);
// Optional.empty
val other: String? = "World"

present?.let { other }
// World

present?.let { absent }
// null

present?.let { null } 
	?: throw NullPointerException()
// throws NullPointerException

absent?.let { other }
// null

absent?.let { absent }
// null

absent?.let { null }
// null

filter()

The filter() method transforms a value to an empty() if a predicate isn’t matched. The Kotlin way of filtering a nullable value is to use takeIf().

present.filter(it -> it.startsWith("H"));
// Optional[Hello]

present.filter(it -> it.startsWith("T"));
// Optional.empty

absent.filter(it -> it.startsWith("H")); 
// Optional.empty

absent.filter(it -> it.startsWith("T")); 
// Optional.empty

present?.takeIf { it.startsWith("H") }
// "Hello"

present?.takeIf { it.startsWith("T") }
// null

absent?.takeIf { it.startsWith("H") } 
// null

absent?.takeIf { it.startsWith("T") } 
// null

Kotlin gets bonus points for allowing you to invert the predicate with takeUnless().

present?.takeUnless { it.startsWith("H") } // null
present?.takeUnless { it.startsWith("T") } // Hello
absent?.takeUnless { it.startsWith("H") } // null
absent?.takeUnless { it.startsWith("T") } // null

Conditional methods

ifPresent()

The safe call operator ?. in Kotlin allows you to call a function on the value if the value is not null. To make this work similarly to the Consumer that ifPresent() accepts, we’ll use the .also() extension method supplied by the standard library since Kotlin 1.1.

present.ifPresent(it -> {
    System.out.println("The value is " + it);
});

absent.ifPresent(it -> {
    System.out.println("The value is " + it);
});
present?.also {
    println("The value is $it")
}

absent?.also {
    println("The value is $it")
}

isPresent()

Testing for the presence of a value in Kotlin is as easy as comparing it to null.

if (present.isPresent()) {
    System.out.println("It's present");
}

if (!absent.isPresent()) {
    System.out.println("It's absent");
}
if (present != null) {
    println("It's present")
}

if (absent == null) {
    println("It's absent")
}

Unwrapping methods

get()

Since nullable types in Kotlin are not wrapped in another class like Optional, there’s no need for an equivalent of the get() method - just assign the value where you need it.

Again, we have to go out of our way to throw an exception to match the Java API.

String x = present.get();
String y = absent.get();
val x = present
val y = absent ?: throw NoSuchElementException()

orElse()

To provide a default to use when the value is null, use the safe call operator. Notice that the equivalent of orElse(null) is simply to evaluate the value - using the safe call operator in those cases is redundant.

present.orElse("Greetings");
// Hello

present.orElse(null);
// Hello

absent.orElse("Greetings");
// Greetings

absent.orElse(null);
// null
present ?: "Greetings"
// Hello

present
// Hello

absent ?: "Greetings"
// Greetings

absent
// null

orElseGet()

Sometimes you want a default that isn’t a literal. This is especially handy when the default is expensive to compute - you only want to take that performance hit if the Optional is empty. That’s why Java’s Optional provides orElseGet().

In Kotlin, the expression to the right of the safe call operator is only evaluated if the left-hand side is null, so our Kotlin approach looks similar to the previous code listing.

String getDefaultGreeting(boolean value) {
    return value ? "Greetings" : null;
}

present.orElseGet(() -> getDefaultGreeting(true));
// Hello

present.orElseGet(() -> getDefaultGreeting(false));
// Hello

absent.orElseGet(() -> getDefaultGreeting(true));
// Greetings

absent.orElseGet(() -> getDefaultGreeting(false));
// null
fun getDefaultGreeting(value: Boolean) =
        if (value) "Greetings" else null

        
present ?: getDefaultGreeting(true)
// Hello

present ?: getDefaultGreeting(false)
// Hello

absent ?: getDefaultGreeting(true)
// Greetings

absent ?: getDefaultGreeting(false)
// null

orElseThrow()

Since throw is an expression in Kotlin (that is, it can be evaluated), we can use the safe call operator again here.

present.orElseThrow(RuntimeException::new);
// Hello

absent.orElseThrow(RuntimeException::new);
// throws RuntimeException
present ?: throw RuntimeException()
// Hello

absent ?: throw RuntimeException()
// throws RuntimeException

Trade-offs

Kotlin’s nullable types have many distinct advantages over Optional.

  • Because they aren’t wrapped in a class, getting at the actual value is easy.
  • Because they aren’t wrapped in a class, you can’t inadvertently assign a null to an Optional reference when you intended to assign an empty. For example, Optional<String> name = null; instead of Optional<String> name = Optional.empty();
  • The Kotlin expressions are generally more terse than their Java Optional counterparts, except in the cases where we wanted to throw an exception.

There are a few cases when you might still want to use an Optional in Kotlin:

  • You’re using a library with a function that expects an Optional argument or returns it as a result.
  • You’re using a library that doesn’t support null, such as RxJava 2.1

Alternatives

  • Kategory, a functional programming library, includes an Option datatype that looks similar to Scala, using an instance of Some or None to represent the value’s presence.2
  • If you’re looking for a lightweight alternative, there’s also Koptional.1

  1. Thanks to Artem Zinnatullin for pointing this out. [return]
  2. Thanks to Raúl Raja for pointing this out. [return]

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit