Java 8 Optional and objects with dynamic structure. Part 4. Using Optional in transformers and consumers

Dynamic structure
Image source: PIRO4D pixabay.com

In this post I will continue the discussion about Optional<T> in Java 8, started in

Java 8 Optional and objects with dynamic structure. Part 1

and continued in the

Java 8 Optional and objects with dynamic structure. Part 2

and  Java 8 Optional and objects with dynamic structure. Part 3

Source code for these posts you will find on my page in GitHub:

https://github.com/vsirotin/Smartenesse-Java

Let me remind you that we are considering objects with a dynamic structure and their implementation using Java 8 Optional<T>.

In the previous post, we discussed the use of Optional by developing of suppliers.

In this post, we will consider the use of Optional in the two remaining STC (Supply-Transform-Consume) steps: in converters and consumers.

Using Optional in Transformers

The transformer receives an input some object and either modifies it or converts it to some other object. In our case we always have an Optional<T>  as an input object. It can be imagined as a case or container which contains or not contains an object of type T.

You can convert it either into a “real” object of some type, or into a new case (container) with a new object.

We will not consider the trivial version of the transformation Optional<T> into Optional<T>. Then, on an abstract level, all the remaining variants of such a transformation can be expressed in the form of three formulas given below:

T t = f1(Optional<T> opt)

U u = f2(Optional<T> opt)

Optional<U> = f3(Optional<T> opt)

Candidates for roles of transformation functions f1, f2 and f3 – methods from the Optional<T> class are represented in this table:

T U Optional<U>
Optional<T> filter()

map()

map() flatMap()

orElse()

orElseGet()

In the previous posts of this cycle, we have already considered most of these methods. Only filter and flatMap remained unreviewed.
Below in this post, we’ll look at examples of using these methods.

Filtering (using the filter() method)

In the following example, we’ll look at the use of the filter() method, which returns an object only if the case (container) is not empty and the object contained therein satisfies some criterion.

Oracle Documentation tells us about this method

(https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html):

Optional<T> filter(Predicate<? super T> predicate)
If a value is present, and the value matches the given predicate, return an Optional describing the value, otherwise return an empty Optional.

In our case, as an object, we will use a portion of water in a rainwater tank. Without going into the analysis of physical and chemical characteristics, we will assume that the collected water can be either clean (pure) (satisfy the criterion) or not. Our device can controll the quality of water and delivers water only if the collected rainwater is clean.

The simplificated diagram of the device is shown in the figure below.

The behavior of our device we simplify as much as possible, reducing everything to the question: is a portion of water given out in this or that case or not. After this simplification, the semantics of the behavior of the device can be described by this table:

You can find the full codes of this example in the project mentioned at the beginning of the post on GitHuB in package eu.sirotin.example.optional4

First we will get acquainted with the class representing collected rain water:

public class RainWater {

   private final boolean clean;

   public RainWater(boolean clean) {
      this.clean = clean;
   }

   public boolean isClean() {
      return clean;
   }

}

As you can see, using the isClean () method, you can request whether the collected water is clean or not.

This class is used as an input parameter in our device.

The same object but in the “case” is used at the output of the device.

public interface IRainWaterDispenserInput { void setAvailability(@Nullable RainWater rainWater); }
public interface IRainWaterDispenserOutput {
   
   Optional<RainWater> getRainWater();
}

And completely the behavior of the device is described by a composite interface:

public interface IRainWaterDispenser extends IRainWaterDispenserInput, IRainWaterDispenserOutput {}

And again we will prepare first a test to check the correctness of the simulation of the behavior of our device. It is not difficult to see that the expectations in the tests below fully correspond to the behavior table presented above.

public class RainWaterDispenser1Test {
    private IRainWaterDispenser rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterDispenser1();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getRainWater().isPresent());
        assertTrue(rainWaterDispenser.getRainWater().get().isClean());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertFalse(rainWaterDispenser.getRainWater().isPresent());
    }
}

Well, now let’s go to the implementation of our class using Optional<T>.

Here is its full code:

public class RainWaterDispenser implements IRainWaterDispenser{
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<RainWater> getRainWater() {
        return Optional.ofNullable(rainWater).filter(RainWater::isClean);
    }
    
}

Boldface shows the use of the filter method. The criterion is the value returned by the isClean() method.

Also note the use of the ofNullable() and filter()  methods in the call chain. Is not it, looks very elegant?

Transformation – (using the flatMap method)

Suppose that the device considered in the previous example is replaced by another, capable of cleaning contaminated rainwater.

Its simplified scheme is shown below.

And the behavior of the device is described here 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 contaminated rainwater enters the tank.

You can find the full source of the example in package
eu.sirotin.example.optional5

As always, start with the interfaces describing the input and output of the device:

public interface IRainWaterCleanerInput {
   
   void setAvailability(@Nullable RainWater rainWater);
}
public interface IRainWaterCleanerOutput {
   
   Optional<CupOfWater> getCleanedWater();
}

Prepare a test that checks whether the device performs the expected behavior:

public class RainWaterCleanerTest {
    private IRainWaterCleaner rainWaterDispenser;

    @Before
    public void setUp() throws Exception {
        rainWaterDispenser = new RainWaterCleaner();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        rainWaterDispenser.setAvailability(new RainWater(true));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        rainWaterDispenser.setAvailability(null);
        assertFalse(rainWaterDispenser.getCleanedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        rainWaterDispenser.setAvailability(new RainWater(false));
        assertTrue(rainWaterDispenser.getCleanedWater().isPresent());
    }
}

Well, now consider the class itself:

public class RainWaterCleaner implements IRainWaterCleaner {
    @Nullable private RainWater rainWater;

    @Override
    public void setAvailability(@Nullable RainWater rainWater) {
        this.rainWater = rainWater;
    }

    @Override
    public Optional<CupOfWater> getCleanedWater() {
        return Optional.ofNullable(rainWater).flatMap(w->Optional.of(new CupOfWater()));
    }    
}

Using the flatMap method is highlighted in bold in the code. Unlike the map method, this method returns not the object itself but a case (container), which can contain the object or can be empty.

Using of Optional in the consumers

The use of the Optional<T> class in transformers and consumers differs insignificantly, because every transformer is always “a bit of a consumer”. After all, every transformation begins with the “perception” of the input object by a transformer. From my personal point of view, the set of methods that can be used in both situations can be described by the formula:

ConsumerMethods = TransformerMethods + ifPresent()

The documentation on the Oracle page defines the semantics of the ifPresent () method as follows:

void ifPresent(Consumer<? super T> consumer)
If a value is present, invoke the specified consumer with the value, otherwise do nothing.

As you can see, the method does not return any value, but it allows to process the object in the case, if it is present there. If it’s not there, nothing happens.

Let us consider this with the example of another device, which is an extension of the previous with one additional function. The new version of the device can not only clean the rainwater of pollution, but also mix it with certain additives. As in the previous examples, we will not be interested in the physical and chemical details.

The diagram of the device is shown in the figure below:

Complete source code of the example you will find in package

eu.sirotin.example.optional6

First, we define a new class representing the result of mixing:

public class MixedWater extends CupOfWater {
   public MixedWater(CupOfWater water) {}
}

The output of the device is determined by this interface:

public interface IMixerOutput extends IRainWaterCleanerOutput {
   
   Optional<MixedWater> getMixedWater();
}

As an input, we use the interface from the previous example. Then completely input and output of the device is determined by such a joint interface:

public interface IMixer extends IRainWaterCleanerInput, IMixerOutput {}

The behavior of the device is similar to the behavior of the previous device, but instead of clean rainwater, we get clean rainwater with the desired additives.

Let’s define a test to check the correctness of the behavior of our device:

public class MixerTest {
    private IMixer mixer;

    @Before
    public void setUp() throws Exception {
        mixer = new Mixer();
    }

    @Test
    public void testRainWaterAvailableAndClean() {
        mixer.setAvailability(new RainWater(true));
        assertTrue(mixer.getMixedWater().isPresent());
    }
    
 
    @Test
    public void testWaterNotAvailable() {
        mixer.setAvailability(null);
        assertFalse(mixer.getMixedWater().isPresent());
    }
    
    @Test
    public void testRainWaterAvailableNotClean() {
        mixer.setAvailability(new RainWater(false));
        assertTrue(mixer.getMixedWater().isPresent());
    }
}

And here is the implementation of the main class:

public class Mixer extends RainWaterCleaner implements IMixer{
   
   private MixedWater result = null;

   @Override
   public Optional<MixedWater> getMixedWater() {
      super.getCleanedWater().ifPresent(this::mix);
      return Optional.ofNullable(result);

   }
   
   private void mix(CupOfWater water) {
      result = new MixedWater(water);
   }

}

The use of the ifPresent () method is highlighted in bold. As you can see, the input method parameter is a method mix() from our class. It in turn expects an object of type CupOfWater as an input parameter. Note that a case with an object of this type returns the getCleanedWater () method.

Well, that’s all the examples I wanted to consider for the Optional<T> class.

But our discussion about this class is not over yet. In the next post we will sum up and talk a little more about various interesting aspects of this class.