
This is the fifth and final article in the series on the use of the Optional class when processing objects with dynamic structure. The
first article was about ways to avoid NullPointerException in situations where you can’t or don’t want to use Optional.
The second article describes the methods of the Optional class as it appeared in Java 8.
The third describes the methods added to the class in Java 9. In the
fourth article, I introduced a class that extends the capabilities of the Optional class when it fails in data processing in the method we need to handle information about this failure.
In this article we will look at the question of whether it is worth using Optional in all those places where zero values may appear. I will also cite the opinion of Brian Goetz, the architect of the Java v Oracle language about this class, and of course, I will fulfill the promise made in the last article - I will encourage every reader who has read all the articles in the series with a valuable gift.
But it could not be so
Before we proceed to the consideration of practical, practical issues of using Optional in different roles within our classes, I would like to make one small, but from my point of view, necessary, historical digression.
')
When the problem of extending Java with the elements of the OP (Functional Programming) is overdue, the range of possible solutions seemed very wide. In the maximal version, one could try to extend Java with the implementation of the most important paradigms of OP tested by that time in such programming languages as LISP, ML, Haskell, OCaml, F #, Erlang, Clojure and Scala.
Critics of the maximalist approach warned that the implementation of certain paradigms of the OP may hamper the architectural decisions previously adopted in the implementation of the language. In addition, they feared that the introduction of some paradigms too difficult to master would lead to their poor use. And this, in turn, threatened that the whole complex of innovations would be ignored by the majority of Java users.
One way or another, in the end, the approach proclaimed as “pragmatic” won. In Java 8, only the main elements of the OP appeared in the form of threads, functions, lambda and Optional.
Within the framework of the winning approach, the Optional in the official documentation did not claim to be one of the implementation of the paradigm of the monad from the OP. (Not claiming a role does not mean not playing it, see details
here. Instead, the Optional class was positioned in the
official “explanatory” article as a container for potentially zero objects.
And where else to apply it?
And if so, then some users have asked themselves a natural question: It turns out that everywhere where null might appear in a class, should we now use Optional?
The aforementioned Oracle article clarifies “No!”
Namely (in my translation):
“The purpose of the Optional class is not to replace every reference to a potentially null object. The goal is to be able to develop more understandable APIs so that, simply by the signature of the method, determine what Optional is returned. This forces you to actively parse Optional to understand its value. "
So be it. But where, in what "places" or "roles" in the class, could one use Optional?
Optional as field value?
Let's see specifically and start with the class variable (field). Let's see the mentioned article. There is such a code:
public class Computer { private Optional<Soundcard> soundcard; public Optional<Soundcard> getSoundcard() { ... } ... }
Aha So you can use Optional as a class variable?
You can, but here's what you need to remember:
- The Optional class does not implement the Serializeble interface. This means that the variable will not be correctly saved or read from the media using the frameworks used by this property.
- Optional is a so-called value-based object. This implies, in particular, special rules for comparing instances of objects among themselves, which can be read in detail here . This in turn means that some frameworks that use the Reflection mechanisms inside may incorrectly process such fields.
Thus, before using Optional as the custodian of the field value in your class, make sure that potential users of your class will not have to serialize it or process it with non-ready frameworks.
Therefore, some authors advise not to get involved in these potential headaches and store the objects themselves in the fields instead of Optional, remembering when programming a class that they may be null. And only “releasing” the object outwards as a return value place it in the Optional case.
Using Optional in setters
Well, what about the methods for setting potentially null class fields? When should you suggest a method in your API with a signature of type setSomeObject (Optional <SomeObject> opt) and when not?
An obvious case is when you build a facade around an existing class that has a setSomeObject method (SomeObject obj) and obj may be null. In this case, at least in the covering “facade” method, you are explicitly notified to the user about the possibility of using a null object.
In the remaining cases, it is almost always better to create a normal setter-method with a restriction (and possibly a check) that the passed parameter is not null. At the design stage, this limitation can be checked using assert. Whether to check it in production and, if so, how - this is a separate interesting question that goes beyond our discussion.
Thus, after many authors, I urge you not to assume and not to allow null transmission in setters.
As so, attentive reader exclaim. In the second article of the series, the author himself suggested using this interface:
public interface IBoilerInput2 { void setAvailability(@Nullable CupOfWater water, boolean powerAvailable); }
with the water parameter, which can be null, and now it says that it is bad? Yes. Exactly. In order to focus your attention on the main aspect, I allowed myself this in the educational example. But when developing real APIs that other developers can use, this should be avoided. But how?
Let's take a closer look at the situation. Passing null as a parameter value means, as a rule, changing the configuration of an object, disabling part of its capabilities, or switching its behavior to a new mode. Match the appropriate term describing such a
switch .
The interface mentioned above would change something like this:
public interface IBoilerInputX { void setWater(@Nonnull CupOfWater water); void setWaterDisabled(); void setPowerAvailable( boolean powerAvailable); }
The new setWaterDisabled method clearly indicates the impending change in the behavior of the device, in contrast to the previous version in IBoilerInput2, where this was achieved by calling a parameter with a zero value.
Using Optional in a parameter group
In some cases, however, for some conceptual or technical reasons, some group of parameters cannot be broken up into separate pairs of setters and switches, and you just need to pass the entire group of parameters in one call. Very often this happens with class constructors. However, some parameters of the group may be missing, unprepared, etc. - i.e. be null. In this case, using the Optional case is much better than transferring potentially null parameters directly. The likelihood that some parameter mistakenly set in previous calculations will fall inside your class and then cause NPE or incorrect behavior, in this case, is significantly reduced.
Final recommendations
Let's summarize the preliminary results in the form of pragmatic recommendations:
- Use Optional in setter or in methods with a single parameter (only) if you build a facade for a method that explicitly implies a parameter that can be null.
- In most cases, if a parameter can take a zero value, pass it through a setter with a non-zero value restriction and an accompanying method of switching the mode of operation of the object. Inside the switch, zoom in the value of the corresponding field.
- If your method should have many parameters, some of which can take zero values, pass such parameters inside the Optional case. This will force the client calling your method to clearly deal with the situation before “inserting” the object into the case.
"Defective" return values
In the first and second articles of this series, I have long convinced readers that potentially zero return values must be packaged in a case before the release - Optional. Is it always good? Unfortunately, where the light is, there is a shadow.
Complex objects, especially in enterprise systems, are often aggregations or hierarchies of simpler objects that are accessible from the outside by calling getter methods. The fact that an object returns an “empty” object (whether it is packaged in a case or not) often does not mean that there is no such object inside an aggregate or hierarchy. In real systems, this often means that the object is in some sense “inferior” or the configuration of the aggregate-object containing it does not allow its use in the external world. From this point of view, the return of Optional.empty () is justified. However, if we want to use the external framework (without using Reflection or manipulating the baicode), save the object on external media or transmit over the network, the “inferiority” of the object or the inappropriate configuration of the aggregate containing it should not interfere with us. It turns out that getter does not allow us to use the optional return case in this case. How so? What did you fight for?
In fact, everything just got better. Yes, we will have to implement special methods for the persistence of parts of a large aggregate. But at the same time, it is better to assemble them into one separate interface, and methods of implementing business logic into another. Thus, we will be able to separate the business aspect and the technical aspect of using the same object.
Optional instead of "magic" numbers
Analogous to null from the world of objects in the world of numbers are the "magic" numbers. Those. such parameter values that are associated with a radically different semantics than with "normal" numbers.
Even standard Java libraries do this. Documentation for the java.net.Socket class constructor:
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
tells us about the localPort parameter, that if it is set to non-zero, the system will use it. And if the value of the parameter is 0, it will look for a free port for you.
We have a radically different behavior of the method with a non-zero and zero value of the parameter.
To avoid such situations in the future, the classes OptionalInt and OptionalDouble were introduced in Java 8. Use these classes if your method should return a certain number if successful, but non-success is also possible.
And instead of the conclusion - think for yourself!
I dedicated five articles to describing the use cases for the Optional class methods. I think in Java there are not so many classes that cause so many questions and disputes.
And by the way, dear readers, how do you think, how many lines of code (apart from comments and with them) does this class contain?
The answer to the question. You will surely be surprisedThe Optional class contains only 100 lines of code, not counting comments. Comments take up 250 lines.
Some of the articles in this series have generated very sharp and interesting discussions. But this is not even a drop in the sea, but a drop molecule compared to discussions and questions that appeared in the English language sector of the Boarding School after the appearance of the Optional in Java 8. One of the hottest and most interesting discussions about the Optional was this:
Should Java 8 getters return optional type ?Brian Goetz, taking the position of Java Language Architect in Oracle, took part in the discussion. Seeing how Java programmers use his brainchild, this is what he wrote in the forum (my translation):
Of course, people will do what they want. But we had a clear intention when adding this functionality, and we did not set a goal to create a mechanism like Maybe or Some in other languages. Although many of this is possible and wanted. Our goal was to provide a compact mechanism for determining the types of return values with a clear selection of the “no result” situation as an alternative to using the zero value, which in most cases can cause errors.
For example, you probably should never use it for something that returns an array of results or a list of results; instead, return an empty array or list. You will almost never use it as a field for anything or a method parameter.
I think that its regular use as a return value for getters will definitely be excessive.
In Optional, there is nothing bad about which to avoid it. This is simply not what many would like to see in its place. And so we were very concerned about the risk of overuse.
Handling: NEVER call Optional.get if you cannot prove that it (in this context) is always non-zero. Instead, use one of the safe methods, such as orElse or ifPresent. In retrospect, we can say that instead of get, we would have to provide something like getOrElseThrowNoSuchElementException, which would make its use more clear. But this is Lesson learned.
In other words, the creators of the language do not always expand it as I would like some (advanced) users. Users do not always use new language features as conceived by their creators. And the creators sometimes regret the mistakes made, which are no longer corrected.
So, dear readers, think for yourself, decide for yourself how to use Optional in your projects. And it remains for me to hope that this series of articles and the promised gift, the poster about the Optional, will help you with this.
Poster about OptionalI advise you to print
“Optional Poster Poster” and hang it on the wall, if you have one near your desk. If not, just download and sometimes look into it.
Pay attention to the names of the groups of class methods on the left. They will help you quickly find the desired method.
The nine in the circle indicates that the method is available starting with Java 9.
The last column covers the semantics of the parameters.
The semantics of the types of input parameters (left) and results (right) will allow you to quickly remember what the method does in the case of a zero and non-zero value of the parameter.
For example, for the Optional.of () method:
(x, Ø)=>(O(x),¥)
the non-zero parameter x is displayed in Optional from it, which is displayed as O (x),
and if you try to use a null input parameter (denoted as Ø), you will get Exception, denoted by the Yen symbol: ¥.