Previously I showed an example that transformed a map of query parameters into a SOLR search string. The pre-Java 8 code used a traditional for
loop with a conditional and used a StringBuilder
to incrementally build a string. The Java 8 code streamed over the map entries, mapping (transforming) each entry to a string of the form "key:value"
and finally used a Collector
to join those query fragments together. This is a common pattern in functional-style code, in which a for loop transforms one collection of objects into a collection of different objects, optionally filters some of them out, and optionally reduce the collection to a single element. These are common patterns in the functional style - map, filter, reduce, etc. You can almost always replace a for loop with conditional filtering and reduction into a Java 8 stream with map, filter, and reduce (collect) operations.
But in addition to the stream API, Java 8 also introduced some nice new API methods that make certain things much simpler. For example, suppose we have the following method to remove all map entries for a given set of keys. In the example code, dataCache
is a ConcurrentMap
and deleteKeys
is the set of keys we want to remove from that cache. Here is the original code I came across:
public void deleteFromCache(Set<String> deleteKeys) {
Iterator<Map.Entry<String, Object>> iterator = dataCache.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
if (deleteKeys.contains(entry.getKey())) {
iterator.remove();
}
}
}
Now, you could argue there are better ways to do this, e.g. iterate the delete keys and remove each mapping using the Map#remove(Object key)
method. For example:
public void deleteFromCache(Set<String> deleteKeys) {
for (String deleteKey : deleteKeys) {
dataCache.remove(deleteKey);
}
}
The code using the for
loop certainly seems cleaner than using the Iterator
in this case, though both are functionally equivalent. Can we do better? Java 8 introduced the removeIf
method as a default method, not in Map
but instead in the Collection
interface. This new method "removes all of the elements of this collection that satisfy the given predicate", to quote from the Javadocs. This method accepts one argument, a Predicate
, which is a functional interface introduced in Java 8, and which can therefore be used in lambda expressions. Let's first implement this a regular old anonymous inner class, which you can always do even in Java 8. It looks like:
public void deleteFromCache(Set<String> deleteKeys) {
dataCache.entrySet().removeIf(new Predicate<Map.Entry<String, Object>>() {
@Override
public boolean test(Map.Entry<String, Object> entry) {
return deleteKeys.contains(entry.getKey());
}
});
}
As you can see, we first get the map's entry set via the entrySet
method and call removeIf
on it, supplying a Predicate
that tests whether the set of deleteKeys
contains the entry key. If this test returns true, the entry is removed. Since Predicate
is annotated with @FunctionalInterface
it can act as a lambda expression, a method reference, or a constructor reference according to the Javadoc. So let's take the first step and convert the anonymous inner class into a lambda expression:
public void deleteFromCache(Set<String> deleteKeys) {
dataCache.entrySet().removeIf((Map.Entry<String, Object> entry) ->
deleteKeys.contains(entry.getKey()));
}
In the above, we've replaced the anonymous class with a lambda expression that takes a single Map.Entry
argument. But, Java 8 can infer the argument types of lambda expressions, so we can remove the explicit (and a bit noisy) type declarations, leaving us with the following cleaner code:
public void deleteFromCache(Set<String> deleteKeys) {
dataCache.entrySet().removeIf(entry -> deleteKeys.contains(entry.getKey()));
}
This code is quite a bit nicer than the original code using an explicit Iterator
. But what about compared to the second code example that looped through the keys using a simple for
loop, and calling remove
to remove each element? The lines of code really aren't that different, so assuming they are functionally equivalent then perhaps it is just a style preference. The explicit for loop is a traditional imperative style, whereas the removeIf
has a more functional flavor to it. If you look at the actual implementation of removeIf
in the Collection
interface, it actually uses an Iterator
under the covers, just as with the first example in this post.
So practically there is no difference in functionality. But, removeIf
could theoretically be implemented for certain types of collections to perform the operation in parallel, and perhaps only for collections over a certain size where it can be shown that parallelizing the operation has benefits. But this simple example is really more about separation of concerns, i.e. separating the logic of traversing the collection from the logic that determines whether or not an element is removed.
For example, if a code base needs to remove elements from collections in many difference places, chances are good that it will end up having similar loop traversal logic intertwined with remove logic in many different places. In contrast, using the removeIf
function leads to only having the remove logic in the different locations - and the removal logic is really your business logic. And, if at some later point in time the traversal logic in the Java collections framework were to be improved somehow, e.g. parallelized for large collections, then all the locations using that function automatically receive the same benefit, whereas code that combines the traversal and remove logic using explicit Iterator
or loops would not.
In this case, and many others, I'd argue the separation of concerns is a much better reason to prefer functional style to imperative style. Separation of concerns leads to better, cleaner code and easier code re-use precisely since those concerns can be implemented separately, and also tested separately, which results in not only cleaner production code but also cleaner test code. All of which leads to more maintainable code, which means new features and enhancements to existing code can be accomplished faster and with less chance of breaking existing code. Until the next post in this ad-hoc series on Java 8 features and a functional style, happy coding!