Method references

Lucky that we have Map, which pairs order items with product information, so we can invoke get on Map:

.map(piMap::get)

The map method is again something that has the same name as something else in Java and should not be confused. While the Map class is a data structure, the map method in the Stream interface performs mapping of the stream elements. The argument of the method is a Function (recall that this is a functional interface we recently discussed). This function converts a value, T, which is available as the element of the original stream (Stream<T>) to a value, R, and the return value of the map method is Stream<R>. The map method converts Stream<T> to Stream<R> using the given Function<T,R>, calling it for each element of the original stream and creating a new stream from the converted elements.

We can say that the Map interface maps keys to values in a data structure in a static way, and the Stream method, map, maps one type of values to another (or the same) type of values dynamically.

We have already seen that we can provide an instance of a functional interface in the form of a lambda expression. This argument is not a lambda expression. This is a method reference. It says that the map method should invoke the get method on Map piMap using the actual stream element as an argument. We are lucky that get also needs one argument, aren't we? We could also write as follows:

.map( orderItem ->piMap.get(orderItem))

However, this would have been exactly the same as piMap::get.

This way, we can reference an instance method that works on a certain instance. In our example, the instance is the one referenced by the piMap variable. It is also possible to reference static methods. In this case, the name of the class should be written in front of the :: characters. We will soon see an example of this when we will use the static method, nonNull, from the Objects class (note that the class name is in plural, and it is in the java.util package and not java.lang).

It is also possible to reference an instance method without giving the reference on which it should be invoked. This can be used in places where the functional interface method has an extra first parameter, which will be used as the instance. We have already used this in Chapter 3, Optimizing the Sort - Making Code Professional, when we passed String::compareTo, when the expected argument was a Comparator. The compareTo method expects one argument, but the compare method in the Comparator interface needs two. In such a situation, the first argument will be used as the instance on which compare has to be invoked and the second argument is passed to compare. In this case, String::compareTo is the same as writing the lambda expression (String a, String b) -> a.compareTo(b).

Last but not least, we can use method references to constructors. When we need a Supplier of (let's be simple) Object, we can write Object::new.

The next step is to filter out the null elements from the stream. Note that, at this point, the stream has ProductInformation elements:

.filter(Objects::nonNull)

The filter method uses Predicate and creates a stream that contains only the elements that match the predicate. In this case, we used the reference to a static method. The filter method does not change the type of stream. It only filters out the elements.

The next method we apply is a bit anti-functional. Pure functional stream methods do not alter the state of any object. They create new objects that they return but, other than that, there is no side effect. peek itself is no different because it only returns a stream of the same elements as the one it is applied on. However, this no-operation feature lures the novice programmer to do something non-functional and write code with side-effects. After all, why use it if there is no (side) effect in calling it?

.peek(pi -> { 
if (pi.getCheck() == null) {
log.info("Product {} has no annotation", pi.getId());
}
})

While the  peek method itself does not have any side effects, the execution of the lambda expression may have. However, this is also true for any of the other methods. It is just the fact that, in this case, it is more tempting to do something inadequate. Don't. We are disciplined adults. As the name of the method suggests, we may peek into the stream but we are not supposed to do anything else. With programming being a particular activity, in this case, peeking, is adequate. And that is what we actually do in our code: we log something.

After this, we get rid of the elements that have no ProductInformation; we also want to get rid of the elements that have, but there is no checker defined:

.filter(pi ->pi.getCheck() != null)

In this case, we cannot use method references. Instead, we use a lambda expression. As an alternative solution, we may create a boolean hasCheck method in ProductInformation, which returns true if the private field check is not null. This would then read as follows:

.filter(ProductInformation::hasCheck)

This is totally valid and works, although the class does not implement any functional interface and has many methods, not only this one. However, the method reference is explicit and specifies which method to invoke.

After this second filter, we log the elements again:

.peek(pi -> log.info( 
"Product {} is annotated with class {}", pi.getId(),
pi.getCheck()))

The next method is flatMap and this is something special and not easy to comprehend. At least for me, it was a bit more difficult than understanding map and filter when I learned functional programming:

.flatMap(pi ->pi.getCheck().stream())

This method expects that the lambda, method reference, or whatever is passed to it as an argument, creates a whole new stream of objects for each element of the original stream the method is invoked on. The result is, however, not a stream of streams, which also could be possible, but rather the returned streams are concatenated into one huge stream.

If the stream we apply it to is a stream of integer numbers, such as 1, 2, 3, ..., and the function for each number n returns a stream of three elements n, n+1, and n+2, then the resulting stream, flatMap, produces a stream containing 1, 2, 3, 2, 3, 4, 3, 4, 5, 4, 5, 6, and so on.

Finally, the stream we have should be collected to a Set. This is done by calling the collector method:

.collect(Collectors.toSet());

The argument to the collector method is (again a name overuse) Collector. It can be used to collect the elements of the stream into some collection. Note that Collector is not a functional interface. You cannot just collect something using a lambda or a simple method. To collect the elements, we definitely need some place where the elements are collected as the ever-newer elements come from the stream. The Collector interface is not simple. Fortunately, the java.util.streams.Collectors class (again note the plural) has a lot of static methods that create and return Object that create and return Collector objects.

One of these is toSet, which returns a Collector that helps collect the elements of the stream into a Set. The collect method will return the Set when all the elements are there. There are other methods that help collect the stream elements by summing up the elements, calculating the average, or to a List, Collection, or to a Map. Collecting elements to a Map is a special thing, since each element of a Map is actually a key-value pair. We will see the example for that when we look at ProductInformationCollector.

The ProductInformationCollector class code contains the collectProductInformation method, which we will use from the Checker class as well as from the ProductsCheckerCollector class:

private Map<OrderItem, ProductInformation> map = null; 

public Map<OrderItem, ProductInformation>
collectProductInformation(Order order) {
if (map == null) {
map = new HashMap<>();
for (OrderItem item : order.getItems()) {
final ProductInformation pi =
lookup.byId(item.getProductId());
if (!pi.isValid()) {
map = null;
return null;
}
map.put(item, pi);
}
}
return map;
}

The simple trick is to store the collected value in Map, and if that is not null, then just return the already calculated value, which may save a lot of service calls in case this method is called more than once handling the same HTTP request.

There are two ways of coding such a structure. One is checking the non-nullity of the Map and returning if the Map is already there. This pattern is widely used and has a name. This is called guarding if. In this case, there is more than one return statement in the method, which may be seen as a weakness or anti-pattern. On the other hand, the tabulation of the method is one tab shallower.
It is a matter of taste and in case you find yourself in the middle of a debate about one or the other solution, just do yourself a favor and let your peer win on this topic and save your stamina for more important issues, for example, whether you should use streams or just plain old loops.

Now, let's see how we can convert this solution into a functional style:

public Map<OrderItem, ProductInformation> collectProductInformation(Order order) { 
if (map == null) {
map =
order.getItems()
.stream()
.map(item -> tuple(item, item.getProductId()))
.map(t -> tuple(t.r, lookup.byId((String) t.s)))
.filter(t -> ((ProductInformation)t.s).isValid())
.collect(
Collectors.toMap( t -> (OrderItem)t.r,
t -> (ProductInformation)t.s
)
);
if (map.keySet().size() != order.getItems().size()) {
log.error("Some of the products in the order do not have product information, {} != {} ",map.keySet().size(),order.getItems().size());
map = null;
}
}
return map;
}

We use a helper class, Tuple, which is nothing but two Object instances named r and s. We will list the code for this class later. It is very simple.

In the streams expression, we first create the stream from the collection, and then we map the OrderItem elements to a stream of OrderItem and productId tuples. Then we map these tuples to tuples that now contain OrderItem and ProductInformation. These two mappings could be done in one mapping call, which would perform the two steps only in one. I decided to create the two to have simpler steps in each line in a vain hope that the resulting code will be easier to comprehend.

The filter step is also nothing new. It just filters out invalid product information elements. There should actually be none. It happens if the order contains an order ID to a non-existent product. This is checked in the next statement when we look at the number of collected product information elements to see that all the items have proper information.

The interesting code is how we collect the elements of the stream into a Map. To do so, we again use the collect method and also the Collectors class. This time, the toMap method creates the Collector. This needs two Function resulting expressions. The first one should convert the element of the stream to the key and the second should result in the value to be used in the Map. Because the actual type of the key and the value is calculated from the result of the passed lambda expressions, we explicitly have to cast the fields of the tuple to the needed types.

Finally, the simple Tuple class is as follows:

public class Tuple<R, S> { 
final public R r;
final public S s;

private Tuple(R r, S s) {
this.r = r;
this.s = s;
}

public static <R, S> Tuple tuple(R r, S s) {
return new Tuple<>(r, s);
}
}

There are still some classes in our code that deserve to be converted to functional style. These are the Checker and CheckerHelper classes.

In the Checker class, we can rewrite the isConsistent method:

public boolean isConsistent(Order order) { 
Map<OrderItem, ProductInformation> map =
piCollector.collectProductInformation(order);
if (map == null) { return false; }
final Set<Class<? extends Annotation>> annotations =
pcCollector.getProductAnnotations(order);
return !checkers.stream().anyMatch(
checker -> Arrays.stream(
checker.getClass().getAnnotations()
).filter(
annotation ->
annotations.contains(
annotation.annotationType())
).anyMatch(
x ->
checker.isInconsistent(order)
));
}

Since you have already learnt most of the important stream methods, there is hardly any new issue here. We can mention the anyMatch method, which will return true if there is at least one element so that the Predicate parameter passed to anyMatch is true. It may also need some accommodation so that we could use a stream inside another stream. It very well may be an example when a stream expression is overcomplicated and needs to split up into smaller pieces using local variables.

Finally, before we leave the functional style, we rewrite the containsOneOf method in the CheckHelper class. This contains no new elements and will help you check what you have learned about map, filter, flatMap, and Collector. Note that this method, as we discussed, returns true if order contains at least one of the order IDs given as strings:

public boolean containsOneOf(String... ids) { 
return order.getItems().stream()
.map(OrderItem::getProductId)
.flatMap(itemId -> Arrays.stream(ids)
.map(id -> tuple(itemId, id)))
.filter(t -> Objects.equals(t.s, t.r))
.collect(Collectors.counting()) > 0;
}

We create the stream of the OrderItem objects, and then we map it to a stream of the IDs of the products contained in the stream. Then we create another stream for each of the IDs with the elements of the ID and one of the string IDs given as the argument. Then, we flatten these substreams into one stream. This stream will contain order.getItems().size() times ids.length elements: all possible pairs. We will filter out those pairs that contain the same ID twice, and finally, we will count the number of elements in the stream.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.140.197.136