Kotlin Nullable Types vs. Java Optional

When Java 8 introduced the Optional type many years ago, I was happy like a bird. I already knew about the Optional type from various adventures into functional programming languages and knew of its powers.

When I learned about Kotlin last year, it seemed to have some similarities to Scala, making me very excited. Initially I was surprised that Kotlin did not embrace the Optional type like Scala did. Of course, there was a very good reason. Kotlin natively supports nullable types, making the Optional type, as well as all the API it provides, obsolete.

In this post we will see how the Optional type compares and translates to nullable types in Kotlin. Prior knowledge about both Optional and nullable types is assumed.

Optional/Nullable Properties and Return Types

Depending on whom you ask, using Optional as the type of a field or return type is bad practice in Java. Fortunately, in Kotlin there is no arguing about it. Using nullable types in properties or as the return type of functions is considered perfectly valid and idiomatic in Kotlin.

In Java, an Optional property can be declared as follows:

public interface Person {
String getName();
Optional<Integer> getAge();
}

Instead of using the Optional type, in Kotlin we will just declare the age property as nullable:

interface Person {
val name: String
val age: Int?
}

Optional.map() vs. Safe-Call Operator

The Optional type’s map() method can be used to access properties of the encapsulated instance in an “empty-safe” manner.

public interface Car {
Person getDriver();
}
Optional<Car> car = getNextCarIfPresent();

Optional<Integer> driversAge =
car.map(Car::getDriver).flatMap(Person::getAge);

This code snippet retrieves the driver from an Optional car and then retrieves that driver’s optional age. Since both, the car and the age are optional, we are forced to use the flatMap() method when retrieving the driver’s age. In Kotlin we can use the built-in safe-call operator ?. to access properties of nullable types:

interface Car {
val driver: Person
}
val car: Car? = getNextCarIfPresent()

val
driversAge: Int? = car?.driver?.age

Optional.map() vs. let() Function

Sometimes we want to use an external method within the chain of safe calls on the Optional type. With Java’s Optional type, the same map() method can be used for this purpose:

Optional<DriversLicence> driversLicence =
car.map(Car::getDriver).map(licenceService::getDriversLicence);

In Kotlin we will have to use the stdlib’s let() function to invoke external functions within a chain of safe-call operators to achieve the same goal.

val driversLicence: DriversLicence? = car?.driver?.let {
licenceService.getDriversLicence(it)
}

Optional.orElse() vs. Elvis Operator

When we retrieve the value wrapped by an Optional, we often want to provide a fallback value. This is achieved by using Optional type’s orElse() method:

boolean isOfLegalAge =
car.map(Car::getDriver).flatMap(Person::getAge).orElse(0) > 18;

When the chain of map calls returns a non-empty age, it will be used. Otherwise the provided fallback value 0 will be used. Kotlin has the built-in elvis operator ?: for this purpose:

val isOfLegalAge: Boolean = car?.driver?.age ?: 0 > 18

Optional.get() vs. Assertion Operator

If we are sure that an Optional value is not empty, we might prefer using Optional type’s assertive get() method instead. We can omit a fallback value and the get() method will throw an exception for us in case of an empty value:

boolean isOfLegalAge =
car.map(Car::getDriver).flatMap(Person::getAge).get() > 18;

In Kotlin we can simply use the built-in assertion operator !! for this:

val isOfLegalAge: Boolean = car?.driver?.age!! > 18

Optional.filter() vs. takeIf() Function

The optional type provides the filter() method, that can be used to check a condition on the wrapped value. If the condition is satisfied by the wrapped value, the filter method will return the same Optional object. Otherwise it will return an empty Optional object.

Optional<Person> illegalDriver =
car.map(Car::getDriver).filter(p -> p.getAge().orElse(0) < 18);

In Kotlin we use the stdlib’s takeIf() function and end up with much less code:

val illegalDriver: Person? = car?.driver?.takeIf { it.age ?: 0 < 18}

Optional.ifPresent() vs. let() Function

At the end of converting and filtering the Optional value, we often want to use the wrapped value in some computation or code. For this purpose, the Optional type provides the ifPresent() method:

car.map(Car::getDriver)
.filter(person -> person.getAge().orElse(0) < 18)
.ifPresent(illegalDriver -> {
checkIdentity(illegalDriver);
putInJail(illegalDriver);
});

In Kotlin we would again use the let() function for this:

car?.driver?.takeIf { it.age ?: 0 < 18 }?.let { illegalDriver ->
checkIdentity(illegalDriver)
putInJail(illegalDriver)
}

Conclusion

Kotlin provides a range of built-in operators and stdlib functions that simplify handling of nullable types. Using them leads to short, concise, and readable code, especially when combined in longer call chains.

Besides the more readable code, Kotlin’s nullable types have several advantages over Java’s Optional type.

First, there is no runtime overhead involved when using nullable types in Kotlin¹. Unlike Optional, no wrapper object is created for wrapping the actual value.

Second, nullable types in Kotlin provide null-safety at compile-time. Using them in a non-safe way will lead to a compile-time error. In contrast, using the Optional type in a non-safe way is not checked by the compiler, and will lead to a runtime exception.

¹ unless we are dealing with nullable primitive types, in which case the boxed version of the primitive type is used on JVM level.