Java 8 has been the most influential update of Java in its history. That version has seen the introduction of lots of features, such as Parallel Sorting, the new java.time package, and so on. Most notably though, it has introduced two incredibly important entities: Streams and Optionals. With this, they have opened Java to the world of functional programming (to a degree), and almost no company will hire a Java developer that is not well versed in Java 8. The newer, shiny versions do exist, certainly, but they do not nearly carry the same importance. Even though Java 8 is no longer a premier LTS (long time support) version (it expired in March 2022), its extended support is currently planned until December 2030, which is 4 years longer than 11, and just over 1 year longer than the newest LTS version, 17!
This article will cover what an Optional in Java is, and then compare it to the alternative that Kotlin has for it, the Nullable.
Java Optionals
Now, I do not want to talk about how cool Java is, nor about ALL new features of Java 8. Streams are fairly commonplace in Java now, and used in lots of places.
Optionals, however, are a different animal. Now, I tend to use Optional a lot when working in Java. I love it. It forces the developers to always think about what should potentially happen if the value is not provided. And there’s no excuse for not knowing that the value might not be present - it was wrapped in an Optional, and not some unexpected null value.
For some reason however, I do not see Optional used nearly as much as Streams. The amount of “if(a == null){…}” I see in every project is staggering sometimes. On one layer, this is still fine. But seeing something like:
private fun uglyNPE() {
if (friend != null) {
if (friend.friend != null) {
if (friend.friend.friend != null) {...}
}
}
}
Now, that’s just ugly.
I rarely ask developers about Streams in interviews - I assume they just know what they’re about. I always ask about Optionals though. Even some more seasoned developers with multiple years of experience are often quite unconvincing in their knowledge about the matter.
In Kotlin, Optionals are not a thing. Same as the entire Stream API isn’t really a thing either - the methods on Streams are simply included in the Kotlin Collections. Similarly, Optionals do not natively exist. They can of course be used, as everything form Java can be used in Kotlin. Just because it can be used however, does not mean that it should!
Nullable in Kotlin
The (better) alternative that Kotlin introduces are Nullables. Along with some other Kotlin introductions like val, they strive to make the dreaded NPE a relic of the past. You can still force NPE in Kotlin, with funky code as shown below, but the frequency of these occurrences are just much less frequent!
data class Person(
val name: String? = null
) {
private fun uglyNPE() {
println(Person().name!!)
}
}
So, what are Nullables, and how do they compare to Optionals?
Well, in short, a Nullable is any attribute that could be null. A small difference to the Optional though is that
the value is not wrapped, but just suffixed with ?
during declaration.
This way, the compiler will know that this is potentially null and pass that information down to any place that accesses that value. If the fact that the variable could be null is not respected there, the compiler will immediately complain.
For example, the following will NOT compile:
data class Person(
val name: String? = null
) {
private fun duplicatePersonName() {
Person().name.contains("a")
}
}
You can see that by the small red warning in the screenshot behind the .
.
This effectively means that, by simply using the ?, the developers will know whether any value is ever allowed to be null, and if so, handle that correctly during development time.
In essence, this is not very different from the consistent usage of Optional, but it is definitely more encouraged by the language itself.
API comparison
Now, one other advantage of using Optional in Java is that it offers an elegant API, for example a nice way of mapping hierarchies. So, let’s see how other operations on Optionals are covered in Nullables.
Presence checking
The simplest start would be checking the simple presence using isPresent()
. In Java, this would be:
Person person = new Person();
if (person.getFriend().isPresent()){
Person friend = person.getFriend().get();
}
In Kotlin, this is a lot easier:
val person = Person()
if (person.friend != null) {
val friend = person.friend // smart type conversion
}
There are two main differences in my opinion:
- .isPresent() vs != null
- .get() required, even though we already performed the check and know that it’s not null. Kotlin automatically converts Person? to Person after its check!
Conversions
.map() / .flatMap() and .orElse vs ?. and ?:
The example above with the triple null check could be effectively replaced by:
return friend.flatMap(Person::getFriend)
.flatMap(Person::getFriend)
.flatMap(Person::getFriend)
.orElseGet(Person::new);
It is quite verbose though.
This would in Kotlin simply be:
return friend?.friend?.friend ?: Person()
Essentially, .map
/ .flatMap
are replaced by ?
, and .orElse
/ .orElseGet
by the elvis operator.
There’s no need between the distinction of .map
and .flatMap
, as once a Nullable has been reached in the chain,
all downstream access are nullables too - no need for a “double null check”.
Consumption
Let’s now assume that Person also has a salary. They want to spend their friend’s salary, which is a void method. In Java, this would look as follows:
person.getFriend().map(Person::getSalary)
.ifPresent(salaryService::spend);
In Kotlin, we use a Scope operator for this:
person.friend?.salary?.let(salaryService::spend)
Keep in mind that .let{}
actually returns something, but since we don’t need that value, it works just fine as is.
Here, Java and Kotlin look very similar.
Without method references, Kotlin would again be a bit more readable in my opinion, due to the it
keyword in lambdas:
- Java ->
person.getFriend().map(Person::getSalary).ifPresent(s -> salaryService.spend(s));
- Kotlin ->
person.friend?.salary?.let { salaryService.spend(it) }
Transformers / Outer mappers
If we slightly change the method above, and we assume the action of spending your friend’s salary returns a value we want to keep (e.g. an enum describing your relationship), we need to use a different keyword in Java.
.map() vs .let{}
For this, in Java we’d do the following:
String relation = person.getFriend().map(Person::getSalary).map(salaryService::spend).orElse("Work on people skills");
In Kotlin, we can use the .let{}
again, as this actually returns the item on the latest line (as was mentioned earlier, and
it will be covered more in a separate article).
val relation: String = person.friend?.salary?.let(salaryService::spend) ?: "Work on people skills"
The similarity between the two becomes slightly clearer if we make them multilines:
-
Java
String relation = person.getFriend() .map(Person::getSalary) .map(s -> { System.out.println("Not nice to spend other people's money"); return salaryService.spend(s); }).orElse("Work on people skills");
-
Kotlin
val relation: String = person.friend ?.salary ?.let { println("Not nice to spend other people's money") salaryService.spend(it) } ?: "Work on people skills"
let
works similarly to .map()
, in that it returns the value of the last line. It simply omits the return keyword
that’s present in .map()
.
Note that the specification of relation being a String is not actually required, but it’s only here for illustrative purposes.
.get() vs !!
Now, this may be a bit more controversial…
In my opinion, this should always be avoided (unless the potential failing
is wanted behaviour). Generally though, you should know whether you have an Object
or you do not. In Java, it may be
required more often, since you need to call .get()
after the .isPresent()
check. In Kotlin however, there is almost
never a usecase where !!
is required.
-
Java:
person.getFriend().get()
-
Kotlin:
person.friend!!
Filtering
This is more widely used in Streams, but it does exist in Optional also. In Streams, it returns 0 to n items for n items in the Stream, and for Optional, it does the exact same. The only difference is that n is always equal to 1 .
In Kotlin, there are even two different functions that .filter()
translates to - both the positive and negative case.
-
Java:
person.getFriend().filter(fr -> fr.name.contains("Joe")); erson.getFriend().filter(fr -> !fr.name.contains("Karen"));
-
Kotlin:
person.friend?.takeIf { it.name.contains("Joe") } person.friend?.takeUnless { it.name.contains("Karen") }
Okay, those are the full APIs of Optionals as well as Nullables (except for the entire Scope operators besides let). I
would argue that Kotlin is, aside from being more concise, also more consistent in the naming. For example, is it really
necessary to have .ifPresent()
, or could you simply use some generic keyword that emcompasses both .map
and ifPresent
?
That is definitely a question of taste. Additionally, the fact that so many operations can be done with ?
is simply very
convenient, as there are multiple choices to make for in Java.