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:
|
|
Creation methods
empty()
The Kotlin equivalent of assigning an empty()
is to assign a 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.
|
|
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.
|
|
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.
|
|
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:
|
|
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
).
|
|
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()
.
|
|
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.
|
|
isPresent()
Testing for the presence of a value in Kotlin is as easy as comparing it to null
.
|
|
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.
|
|
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.
|
|
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.
|
|
orElseThrow()
Since throw
is an expression in Kotlin (that is, it can be evaluated), we can use the safe call operator again here.
|
|
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 anOptional
reference when you intended to assign anempty
. For example,Optional<String> name = null;
instead ofOptional<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