Object in a case or Optional in Java 8 and Java 9. Part 2: “How it is done in Java 8”
- Tutorial
Optional class is devoted a lot of articles and tutorials, including this and this on Habré.
Most of them tell how methods of this class are called. In this tutorial I emphasize why, why, in what cases it is possible (or rather even necessary) to use one or another class method. I think this is very important, because, as the survey showed after the first article in this tutorial, not all Java programmers are in the taste of using all the power of the methods of this class.
For a better explanation of the class methods, I will use more complex and illustrative examples than in most other tutotials — a coffee maker, a filtration unit, a mixer, etc.
This is the second 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.
In this article, we will look at all the class methods as provided by Java 8. The class extensions in Java 9 are discussed in the third article in this series. The fourth article is devoted to the necessary (from the point of view of the author) addition to this class.
Well, in the fifth article I tell about where the Optional should be used inside the class, summarize and present a valuable gift to every reader who has read the series to the end.
In this tutorial there will be a lot of source code, including Junit tests. I share the opinion of some of my colleagues on the pen that reading the test code helps to better master the material. You'll find all the source code in my project on GitHub .
So, in the first article of this series I tried to consider approaches that you can use when implementing objects with dynamic structure and promised to justify why the Optional almost always works better than other approaches in this situation. Let's start fulfilling promises. Let's start with the definition.
What is Optonal?
Before we get to the concrete example, let's try to answer the question, what is Optional?
I would venture to give my own visual definition. In the first approximation, Optional is a software analogue of a case of a physical object, for example, glasses. Whether the object is inside the case, you can find out using the isPresent () method. If it is there, you can take it using the get () method. In general, approximately as shown in the title picture of the series.
So:
In the first approximation, Optional is a case for some object.
Use to a minimum
In our first example, we try using Java 8 Optional to simulate the operation of a device that combines a drinking water faucet and a boiler.
His diagram is shown in the picture below:
As you can see, the device needs water and electricity to operate. At the exit, it can produce raw or boiled water.
Thus, the input of this device can be described with the following interface:
publicinterfaceIBoilerInput{
voidsetAvailability(boolean waterAvailable, boolean powerAvailable);
}
And the output is like this:
publicinterfaceIBoilerOutput{
Optional<CupOfWater> getCupOfWater();
Optional<CupOfBoiledWater> getCupOfBoiledWater();
}
Since (depending on the input data) the device may or may not give out raw and boiled water, we present the result of calling get ... with the help of Optional.
The behavior of the device as a whole describes the interface that combines the methods of entry and exit.
publicinterfaceIBoilerextendsIBoilerInput, IBoilerOutput{}
Classes representing the varieties of water are of little interest to us, so we implement them in a minimal way. This is the class for serving raw water:
publicclassCupOfWater{
public CupOfBoiledWater boil(){returnnew CupOfBoiledWater();}
}
We will assume that a portion of boiled water is a new object, different from raw water. Therefore, we will present it as a separate class:
publicclassCupOfBoiledWater{}
So, the task is set. In the best traditions of TDD (Test-Driven Development), we first write a test to check whether we simulated the behavior of our simple device:
JUnit test Boiler1Test
publicclassBoiler1Test{
private IBoiler boiler;
@BeforepublicvoidsetUp()throws Exception {
boiler = new Boiler1();
}
@TestpublicvoidtestBothNotAvailable(){
boiler.setAvailability(false, false);
assertFalse(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestPowerAvailable(){
boiler.setAvailability(false, true);
assertFalse(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestWaterAvailable(){
boiler.setAvailability(true, false);
assertTrue(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestBothAvailable(){
boiler.setAvailability(true, true);
assertTrue(boiler.getCupOfWater().isPresent());
assertTrue(boiler.getCupOfBoiledWater().isPresent());
}
}
Our test verifies that the appliance actually delivers raw water if water is supplied to the inlet, regardless of the presence of electricity. But the device produces boiled water only if there is both water and electricity.
Before proceeding to the implementation, stop for a moment and think through your head or even behind the keyboard, how would you program this solution within the approaches discussed in the first article of the series:
Using a pair of has ... get ...
C using returning an array or sheet values
using the sign of activity issued to the outside of the product.
If you really tried to imagine this, and even better, try to program the solution of the problem within these approaches, you will certainly appreciate the simplicity and elegance that Java 8 Optional brings to our programming life.
Look at my, certainly not the optimal solution:
publicclassBoiler1implementsIBoiler{
privateboolean waterAvailable;
privateboolean powerAvailable;
@OverridepublicvoidsetAvailability(boolean waterAvailable, boolean powerAvailable){
this.waterAvailable = waterAvailable;
this.powerAvailable = powerAvailable;
}
@Overridepublic Optional<CupOfWater> getCupOfWater(){
return waterAvailable ? Optional.of(new CupOfWater()) : Optional.empty();
}
@Overridepublic Optional<CupOfBoiledWater> getCupOfBoiledWater(){
if(!powerAvailable)return Optional.empty();
return getCupOfWater().map(cupOfWater->cupOfWater.boil());
}
}
Pay attention to the last line of listing where the map () method from the Optional class is used. This way you can build processing chains. If at one of the links in the chain it turns out that no further processing is possible, the entire chain will return an empty response.
The results of our boiler model depend on external conditions, which were set using Boolean variables. But in the majority of practically interesting problems, not simple variables are fed into the input, but objects. Including those that can be null.
Consider how you can apply Optional if the behavior is not determined by Boolean variables, but by “old-mode” objects that admit zero values.
Let the input of our boiler a slightly different model than in the first example is defined as follows:
publicinterfaceIBoilerInput2{
voidsetAvailability(@Nullable CupOfWater water, boolean powerAvailable);
}
The zero value of the water object means that water does not flow into the device from the water supply system.
Then the behavior of the device as a whole is defined by the following interface:
publicinterfaceIBoiler2extendsIBoilerInput2, IBoilerOutput{}
As in the previous example, we define tests that verify the correctness of our implementation:
JUnit test Boiler2Test
publicclassBoiler2Test{
private IBoiler2 boiler;
@BeforepublicvoidsetUp()throws Exception {
boiler = new Boiler2();
}
@TestpublicvoidtestBothNotAvailable(){
boiler.setAvailability(null, false);
assertFalse(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestPowerAvailable(){
boiler.setAvailability(null, true);
assertFalse(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestWaterAvailable(){
boiler.setAvailability(new CupOfWater(), false);
assertTrue(boiler.getCupOfWater().isPresent());
assertFalse(boiler.getCupOfBoiledWater().isPresent());
}
@TestpublicvoidtestBothAvailable(){
boiler.setAvailability(new CupOfWater(), true);
assertTrue(boiler.getCupOfWater().isPresent());
assertTrue(boiler.getCupOfBoiledWater().isPresent());
}
}
If we compare these tests with tests for the boiler of the first model, we will see their very large similarity. Checking the results of the same tests from different sets is the same. Well, the input differs in that instead of true for the water source, we submit the object, and instead of false, -null.
And here is the implementation itself:
publicclassBoiler2implementsIBoiler2{
@Nullableprivate CupOfWater water;
privateboolean powerAvailable;
@OverridepublicvoidsetAvailability(@Nullable CupOfWater water, boolean powerAvailable){
this.water = water;
this.powerAvailable = powerAvailable;
}
@Overridepublic Optional<CupOfWater> getCupOfWater(){
return Optional.ofNullable(water);
}
@Overridepublic Optional<CupOfBoiledWater> getCupOfBoiledWater(){
if(!powerAvailable)return Optional.empty();
return getCupOfWater().map(cupOfWater->cupOfWater.boil());
}
}
As we can see, the Optional.ofNullable () method allows you to elegantly “put” in a case a dangerous object with a potentially zero value. If the object has a zero value, the case will be empty. Otherwise, it contains the object we need.
It is time to sum up the first results and formulate the first rules for minimal use of the Optional:
If your method returns an object that may or may not be present, you “stack” it in Optional. When laying you use the following rules:
Condition Class method used Object missing Optional.empty () The object is present and not exactly null. Optional.of (...) The object is present, but may be null Optional.ofNullable (...)
Whether the object is in a case, you define using the isPresent () method. And if the check is positive, you get the object out of the case with get ().
So we mastered using Optional so that we no longer use null as the return result.
But we will not stop there.
Now we will consider another, not so rare situation when a certain resource is represented by the main and reserve element.
Well, when there is a stash ...
Zanachka is a simple definition for a backup resource. Let us distract from the emotional side of this term and consider the technical side of the question.
In technical systems, resources of the same kind are often available in more than one way.
In the following example, we consider a simple water supply fixture. So arranged irrigation devices used by summer residents. Rainwater is collected in a special container, which is then consumed first. If it is not there or it is over, water is consumed from the water supply system.
We will not complicate the task with unnecessary details about the incomplete filling of the rain tank and its size and simply use the familiar CupOfWater class again.
The input of such a device is described as follows:
publicinterfaceIWaterDispenserInput{
voidsetAvailability(@Nullable CupOfWater firstPortion);
}
If rainwater is not collected, then at the entrance we have a zero object, otherwise - a normal object.
The output of the device is described by the following interface:
publicinterfaceIWaterDispenserOutput{
CupOfWater getCupOfWater();
}
Note that at the output we have a CupOfWater object and not an Optional. We do so in order to more clearly show the mechanism of interest to us. After you, dear readers, understand it, you can easily reprogram an example and receive an Optional output.
The behavior of the device as a whole is determined by the combination of these interfaces:
publicinterfaceIWaterDispenserextendsIWaterDispenserInput, IWaterDispenserOutput{}
As in the previous examples, we first prepare tests to test the behavior of our implementation:
JUnit test WaterDispenser1Test
publicclassWaterDispenser1Test{
private IWaterDispenser waterDispenser;
@BeforepublicvoidsetUp()throws Exception {
waterDispenser = new WaterDispenser1();
}
@TestpublicvoidtestMainAvailable(){
waterDispenser.setAvailability(new CupOfWater());
assertNotNull(waterDispenser.getCupOfWater());
}
@TestpublicvoidtestMainNotAvailable(){
waterDispenser.setAvailability(null);
assertNotNull(waterDispenser.getCupOfWater());
}
}
Our expectations are as follows: the device produces water regardless of whether the tank with rain water is filled or not, because in the latter case the water will be taken from the “reserve” (water supply system).
Consider now the implementation:
publicclassWaterDispenser1implementsIWaterDispenser{
@Nullableprivate CupOfWater mainCup;
@OverridepublicvoidsetAvailability(@Nullable CupOfWater mainCup){
this.mainCup = mainCup;
}
@Overridepublic CupOfWater getCupOfWater(){
return Optional.ofNullable(mainCup).orElse(new CupOfWater());
}
}
As we can see, the orElse method has been added to the ofNullable () method. If the first element returns an empty Optional (no rainwater accumulates), the second method will add an object from itself. If the first method gives a non-empty Optional, the second method simply passes it through itself and the tap water will remain untouched.
This implementation assumed the presence of a backup object. If you need to create an object before this (in our case, pump up water), you can use the orElseGet () method with the Supplier type parameter:
publicclassWaterDispenser2implementsIWaterDispenser{
@Nullableprivate CupOfWater mainCup;
@OverridepublicvoidsetAvailability(@Nullable CupOfWater mainCup){
this.mainCup = mainCup;
}
@Overridepublic CupOfWater getCupOfWater(){
return Optional.ofNullable(mainCup).orElseGet(()->new CupOfWater());
}
}
Do not let the genie out of the bottle
In some cases, restrictions on your API do not allow you to use Optional as a return value.
Suppose that our interface is defined in such a way that the client always expects an object at the output of our function. If the requested resource is not present at the time of the request, and we do not want to return null, we have only one tool left - throw Exception. Thus, we do not release the genie from the bottle - we do not allow the released zero object to turn in the client code NullPoiner Exception.
Can Java 8 Optional help us in this case? Yes maybe.
But before considering the solution, we will prepare a test verifying the correctness of its work:
@Test (expected = IllegalStateException.class)
publicvoidtestMainNotAvailable(){
waterDispenser.setAvailability(null);
waterDispenser.getCupOfWater();
fail("This code line must be not reached");
}
And here is the solution:
publicclassWaterDispenser3implementsIWaterDispenser{
@Nullableprivate CupOfWater mainCup;
@OverridepublicvoidsetAvailability(@Nullable CupOfWater mainCup){
this.mainCup = mainCup;
}
@Overridepublic CupOfWater getCupOfWater(){
return Optional.ofNullable(mainCup).orElseThrow(()->new IllegalStateException("Resource not available"));
}
}
I think many readers will not be convinced by this decision. In fact, how is it better to check for null using if?
The main argument in favor of this decision is the possibility of building chains of functional calls in this way. However, the chain may fail with Exception. In the fourth article of this cycle, I venture to offer my solution to the problem of handling Exception in such chains of functional calls.
It is time to formulate a new group of rules on using Optional for the case when we have several alternatives for creating a dynamic object:
If you have two or more alternatives for creating a dynamic object, use the following rules:
Condition Class method used An alternate object is present. orElse (...) Alternate object must first be created (for example, get from repository) orElseGet (() -> ...) Alternative resource dried up (throw exception) orElseThrow (() -> new IllegalStateException (...))
So far, we have considered using Optional at the stages of creating and using objects with dynamic structure. We now consider the question of how Optional can help us in the transformation of such objects.
Using Optional in Converters
The transformer (Transformer) receives an object as input and either modifies it or converts it into some other object. In our case, since we are limited to using Optional, we always have an Optional as an input object. Recall that this can be imagined as a case or container in which the object is located or not.
You can convert it either to a “real” object of any type, or to a new case with a new object.
At the abstract level, all variants of this transformation can be expressed in the form of the three formulas below:
T t = f1 (Optional <T> opt)
U u = f2 (Optional <T> opt)
Optional <U> = f3 (Optional <T> opt)
Candidates for the roles of transformation functions f1, f2 and f3 - methods from the Optional class are presented in this table:
Candidates for the role of f1 Candidates for the role of f2 Candidates for the role of f3 filter () map () flatMap () orElse () map () orElseGet ()
In the previous posts of this cycle we have already reviewed most of these methods. Only filter and flatMap remained unreviewed.
Below we look at examples of using these methods.
Filtering (using the filter method)
In the following example, we will consider using the filter () method which returns an object only if the case is not empty and the object it contains satisfies some criteria.
In our case, as an object, we will use a portion of water in the tank to collect irrigation of the suburban area. Without going into the analysis of physical and chemical features, we will assume that the collected water can either be clean (meet the criteria) or not.
The most simplified diagram of the device is shown in the figure below.
We simplify the behavior of our device as much as possible, reducing everything to a question: whether a portion of water is given out in this or that case or not. After this simplification, the semantics of the instrument behavior can be described by this table:
You can find all the codes for this example in the project on GitHuB mentioned at the beginning of the article in package eu.sirotin.example.optional4
First, let's get acquainted with the class representing collected rainwater:
publicclassRainWater{
privatefinalboolean clean;
publicRainWater(boolean clean){
this.clean = clean;
}
publicbooleanisClean(){
return clean;
}
}
As you can see, using the isClean () method, you can find out whether the collected water is clean or not.
This class is used as an input parameter in our instrument.
The same object but in the “case” is used at the exit of the device.
publicinterfaceIRainWaterDispenserInput{ voidsetAvailability(@Nullable RainWater rainWater); }
publicinterfaceIRainWaterDispenserOutput{
Optional<RainWater> getRainWater();
}
And the complete behavior of the device is described by a composite interface:
publicinterfaceIRainWaterDispenserextendsIRainWaterDispenserInput, IRainWaterDispenserOutput{}
And again we will first prepare a test to verify the correctness of modeling the behavior of our device. It is easy to see that the expectations in the tests below fully correspond to the behavior table presented above.
JUnit test RainWaterDispenser1Test
publicclassRainWaterDispenser1Test{
private IRainWaterDispenser rainWaterDispenser;
@BeforepublicvoidsetUp()throws Exception {
rainWaterDispenser = new RainWaterDispenser1();
}
@TestpublicvoidtestRainWaterAvailableAndClean(){
rainWaterDispenser.setAvailability(new RainWater(true));
assertTrue(rainWaterDispenser.getRainWater().isPresent());
assertTrue(rainWaterDispenser.getRainWater().get().isClean());
}
@TestpublicvoidtestWaterNotAvailable(){
rainWaterDispenser.setAvailability(null);
assertFalse(rainWaterDispenser.getRainWater().isPresent());
}
@TestpublicvoidtestRainWaterAvailableNotClean(){
rainWaterDispenser.setAvailability(new RainWater(false));
assertFalse(rainWaterDispenser.getRainWater().isPresent());
}
}
Well, now let's proceed to the consideration of the implementation of our class with the help of Optional.
Here is its full text:
publicclassRainWaterDispenserimplementsIRainWaterDispenser{
@Nullableprivate RainWater rainWater;
@OverridepublicvoidsetAvailability(@Nullable RainWater rainWater){
this.rainWater = rainWater;
}
@Overridepublic Optional<RainWater> getRainWater(){
return Optional.ofNullable(rainWater).filter(RainWater::isClean);
}
}
The last line shows the use of the filter () method. As a criterion, the value returned by the isClean () method of the object is used.
Notice also the use of the ofNullable () and filter () methods in the call chain. Doesn't it look very elegant?
Transformation - (using the flatMap method)
Suppose that the device described in the previous example is replaced by another, capable of purifying contaminated rainwater.
Its most simplified diagram is shown below.
And the behavior of the device is described here by such a semantic table:
If we compare this and the previous table, we will see the obvious advantage of the new device: it gives out clean water even if polluted rainwater has arrived at the entrance.
As always, let's start with the interfaces describing the input and output of the device:
publicinterfaceIRainWaterCleanerInput{
voidsetAvailability(@Nullable RainWater rainWater);
}
publicinterfaceIRainWaterCleanerOutput{
Optional<CupOfWater> getCleanedWater();
}
Let's prepare a test verifying whether the device implements the expected behavior from it:
JUnit test RainWaterCleanerTest
publicclassRainWaterCleanerTest{
private IRainWaterCleaner rainWaterDispenser;
@BeforepublicvoidsetUp()throws Exception {
rainWaterDispenser = new RainWaterCleaner();
}
@TestpublicvoidtestRainWaterAvailableAndClean(){
rainWaterDispenser.setAvailability(new RainWater(true));
assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
}
@TestpublicvoidtestWaterNotAvailable(){
rainWaterDispenser.setAvailability(null);
assertFalse(rainWaterDispenser.getCleanedWater().isPresent());
}
@TestpublicvoidtestRainWaterAvailableNotClean(){
rainWaterDispenser.setAvailability(new RainWater(false));
assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
}
}
Well, now consider the class itself:
publicclassRainWaterCleanerimplementsIRainWaterCleaner{
@Nullableprivate RainWater rainWater;
@OverridepublicvoidsetAvailability(@Nullable RainWater rainWater){
this.rainWater = rainWater;
}
@Overridepublic Optional<CupOfWater> getCleanedWater(){
return Optional.ofNullable(rainWater).flatMap(w->Optional.of(new CupOfWater()));
}
}
Using the flatMap () method is shown in the last line. Unlike the map () method, this method does not return the object itself, but a case (container), which may be empty.
Using Optional in object consumers (Consume)
In the first example, we looked at the use of the isPresent () method, which allows us to determine if an object is in a case. If further processing is assumed only if it is present, it is better to use ifPresent (...) instead of isPresent (...)
This method does not return any value, but allows you to process the object in a case if it is present there. If it's not there, nothing happens.
Consider its action on the example of another device, which is a complication of the previous one due to the additional function. The new version of the device can not only purify rainwater from pollution, but also mix it with certain additives. As in the previous examples, we will not be interested in the details about these additives.
The device is shown in the figure below:
First, we define a new class representing the result of mixing:
publicclassMixedWaterextendsCupOfWater{
publicMixedWater(CupOfWater water){}
}
The output of the device is determined by this interface:
publicinterfaceIMixerOutputextendsIRainWaterCleanerOutput{
Optional<MixedWater> getMixedWater();
}
We use the interface from the previous example as input. Then the full input and output of the device is determined by such a joint interface:
publicinterfaceIMixerextendsIRainWaterCleanerInput, IMixerOutput{}
The behavior of the device is similar to the behavior of the previous device, only instead of purified rainwater, we get purified rainwater with the desired additives.
Let's make a test to check the correctness of the behavior of our device:
JUnit test MixerTest
publicclassMixerTest{
private IMixer mixer;
@BeforepublicvoidsetUp()throws Exception {
mixer = new Mixer();
}
@TestpublicvoidtestRainWaterAvailableAndClean(){
mixer.setAvailability(new RainWater(true));
assertTrue(mixer.getMixedWater().isPresent());
}
@TestpublicvoidtestWaterNotAvailable(){
mixer.setAvailability(null);
assertFalse(mixer.getMixedWater().isPresent());
}
@TestpublicvoidtestRainWaterAvailableNotClean(){
mixer.setAvailability(new RainWater(false));
assertTrue(mixer.getMixedWater().isPresent());
}
}
And here is the implementation of the main class:
publicclassMixerextendsRainWaterCleanerimplementsIMixer{
private MixedWater result = null;
@Overridepublic Optional<MixedWater> getMixedWater(){
super.getCleanedWater().ifPresent(this::mix);
return Optional.ofNullable(result);
}
privatevoidmix(CupOfWater water){
result = new MixedWater(water);
}
}
Let's take a closer look at using the ifPresent () method. As we can see, the method from our mix () class is used as the input parameter of the method. He, in turn, expects a CupOfWater object as an input parameter. Note that a case with an object of this type is returned by the getCleanedWater () method.
We formulate the rules for using Optional in consumers (clients).
If the processing of a potentially empty object will be performed only in the positive case (the object is not empty) - use the IfPresent (...) method.
Otherwise, you can find out if the object lies inside the case using the isPresent () method. If it is there, you can take it using the get () method.
Well, these are all the examples that I wanted to consider with reference to the Optional class in Java 8.
But our conversation about this class is not over yet. In the following articles, I will discuss the innovations in this class in Java 9, as well as some of its drawbacks and limitations.
Transition to the third article of this series.
Illustration: ThePixelman