Java Optional Is Not So Obvious
NullPointerException
is such an annoying thing in the Java world, and Optional
was brought to solve the problem. We can’t say that it has absolutely gone, but we made huge steps. Many popular libraries and frameworks put Optional within their ecosystem. For instance, Spring Data
repositories return Optional<Entity>
, instead of null
.
I think that some programmers have become so excited about Optional
that they started to overuse it and apply it with the wrong patterns. In this article I have listed some misuses of this monad and offered my way to handle the given problems.
Optional must never be null
I guess that no additional explanations are required here. Assigning Optional to null
breaks the whole idea of the class. No client of your API will ever check the Optional for null equality. Instead of null
you should prefer to use Optional.empty().
Know the API
Since new Java versions have been released, Optional
was enhanced with new features that allow to achieve less verbose code.
The idea is simple. If the name is empty, return the default one. We can rewrite it in a more pretty way.
Let’s make something more complicated. Suppose, we need to return an Optional
of the person’s name. If the name contains in the set of allowed names, the value should be present, otherwise not. Here is the verbose approach.
Using Optional.filter
can make this code look much better.
This advice applies not only to Optional
but to the whole process of development.
If the language has its own solution, try to use it before coming up with your own.
Prefer value-based alternatives to generic ones
There are special non-generic Optional classes. OptionalInt
, OptionalLong
, and OptionalDouble
. If you need to work with primitive types, it is better to use value-based containers. In this case, there would be no additional boxing and unboxing procedures that might affect performance.
Don’t disregard the laziness
Optional.orElse
is a convenient approach for returning default values. But if the computation of the default value is rather expensive, this can cause some performance issues.
Even if the table is present in the cache, it will be fetched from the remote server each time. Thankfully, this can be easily solved with Optional.orElseGet
.
Optional.orElseGet
accepts lambda that shall be calculated only if it applied on an empty container.
Don’t make Optional from collections
Although I’ve seen that no so often, it happens sometimes.
Any collections is a container itself which emptiness can be determined without additional classes.
Unnecessary Optionals makes the API too hard to work with.
Don’t pass Optional as a parameter
Here we start discussing the most arguable parts of the article.
Why shouldn’t we pass Optional as a parameter? At first glance that seems logical. It helps us to avoid the unexpected NullPointerException
, right? Well, perhaps it is. But the problem has deeper roots.
Firstly, the API has unnecessary bounds. On each invocation a user has to wrap values with Optional. Even known constant values.
Secondly, applySettings
has 4 potential different behaviours that are based on Optionals presence. That breaks the SRP (single responsibility principle).
Finally, we have no idea how does the method interprets an Optional. Does it just replace the absent value with the default one or does it completely change the business logic? Or maybe it just throws NoSuchElementException
.
If we look at the javadoc of Optional, we can find an interesting note.
Optional is primarily intended for use as a method return type where there is a clear need to represent “no result,” and where using null is likely to cause errors.
Optional literally defines possible no result object. Passing possible no result in the method sounds like a bad idea. That means that the API knows too much about the context and makes decisions that it shouldn’t be aware of.
So, how can we improve this code? If age
and role
must always be present, we can just eliminate the Optional from the parameters and handle its absence on the top level.
Now the calling code is in control of the argument values. That becomes even more crucial if you develop a framework or a library.
On the contrary, if age
or role
might be omitted this approach is not going to work. In that case, the best way would be to declare separate methods for different user’s needs.
Perhaps that looks a little verbose, but now a user has the ability to do exactly what they need avoiding unexpected errors.
Don’t use Optional as a class field
I heard many opinions on this point. Some people think that storing Optionals in classes directly helps to reduce NullPointerException
tremendously. My friend who works in a well-known startup says that this approach is considered as a pattern in their company. Others think that this breaks the whole concept of the monad.
Although storing Optional directly may sound like a good idea, I think that this can bring more problems than benefits.
No serializability
Optional does not implement Serializable
interface. That’s not a bug, this was done manually because Optional was designed to use as a return type only. So, the class that has any Optional field cannot be serialized.
I think that this point is the least convincing one. Because in the modern world of distributed systems and microservices platform based serialization has no such great significance as it used to be.
Keeping unnecessary references
Optional is the object that a user needs only for a few milliseconds. After that, it can be deleted by the garbage collector. But if we keep Optional as a class field, it can be stored there till the program stops. You won’t probably notice any performance issues on a small project. Anyway, if we are talking about a large application with dozens of beans that may lead to different consequences.
Poor integration with Spring Data/Hibernate
Suppose we are building a simple Spring Boot application. We need to retrieve values from the table. This can be easily done by declaring the entity and the corresponding repository.
That’s the possible result of personRepository.findAll()
.
Person(id=1, firstName=John, lastName=Brown)
Person(id=2, firstName=Helen, lastName=Green)
Person(id=3, firstName=Michael, lastName=Blue)
Suppose that firstName
and lastName
fields are nullable. We don’t want to mess up with NullPointerException
. So, let’s replace simple field declarations with Optional.
Now it’s all broken.
org.hibernate.MappingException: Could not determine type for: java.util.Optional, at table: person, for columns: [org.hibernate.mapping.Column(firstname)]
Hibernate just cannot map the value from the database to Optional.
But some things do work appropriately
I have to admit, that everything is not so bad after all. Some frameworks integrated with Optional correctly.
Jackson
Let’s declare simple endpoint and DTO.
Here is the result for GET /person/1
.
{
"id": 1,
"firstName": "John",
"lastName": "Brown"
}
As you can see, we have no configuration. Everything works out of the box. Let’s try to replace String
with Optional<String>
In order to test multiple cases, I replaced one assignment with Optional.empty()
.
Surprisingly everything still works correctly.
{
"id": 1,
"firstName": "John",
"lastName": null
}
So, we can use Optional as a field value safely with Spring Web, right? Well, kind of. There are some corner cases.
SpringDoc
SpringDoc is a library for Spring Boot applications that generates Open Api Schema automatically.
Here is what we’ve got for the GET /person/{id}
endpoint.
"PersonDTO": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
Looks pretty decisively. But we need to make id
property mandatory. This can be done by either using @NotNull
or @Schema(required = true)
. Let’s add some more interesting details. What if we put @NotNull
on an Optional field?
That shall lead to interesting results.
"PersonDTO": {
"required": [
"firstName",
"id"
],
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
}
}
}
As we can see, id
is the required field indeed. But firstName
is too. And here starts the tricky part. Optional cannot be the required one because its whole sense means that the value might be omitted. Anyway, we mispointed the framework with only one additional annotation.
What’s the problem? For example, if the frontend part uses any types generator based on Open Api Schema, that would corrupt the data format and might lead to unpleasant consequences.
Solution
What can we do about it? The answer is simple. Use Optional only for getters.
Now this class can be safely used either as DTO or a Hibernate entity. Optional has no affect on the data. It just wraps nullable values to handle absent data appropriately.
But there is one disadvantage. This approach cannot be fully integrated with Lombok. Optional getters are not supported by the library. And it won’t probably happen. At least we can consider that by some discussions on Github.
I wrote an article about Lombok and I think that is a brilliant tool. And the fact that is not integrated with the Optional-Getter pattern is not pleasant.
For now the only work-around is to define getters manually.
Conclusion
That’s all I wanted to say about java.util.Optional
. I know that it’s an arguable topic. If you have any questions or suggestion, please leave your comments. Thanks for reading!