The java.util.concurrent
got a massive overhaul with the release of Java 8.
We’ve been introduced to whole new classes like CompletableFuture
that brought a long awaited improvement to asynchronous work.
We’ve also seen a lot of new methods being introduced in already existing classes and interfaces - ConcurrentHashMap
being one of them. This is to utilize lambdas and support the increased focus on concurrency.
To get familiar with these features — consider the following scenario.
We have a map of tags and their associated articles — resulting in a ConcurrentHashMap<String, List<Article>>
.
Executing a function for each key-value pair
One of the methods that where introduced to a lot of big interfaces — Map
no exception — was the forEach
method.
It basically iterates your data — in our case the key-value pairs — executing a function of type Consumer
.
A Consumer
doesn’t return anything, so any changes you want to happen have to be side effects of your Consumer
.
Let's give it a go by printing each tag and the number of associated articles.
map.forEach((k, v) -> System.out.println(k + "contains " + v.size() + " articles"));
Adding parallelism threshold
ConcurrentHashMap
is all about concurrency and parallelism. So it shouldn’t be of any surprise that you’ll find ways of controlling how this parallelism should work.
Through an extra parameter — parallelism threshold— we can say how many elements are needed for an operation to be executed in parallel.
The parallelism threshold is available in most of the new methods — including forEach
.
Let’s see how this variant of forEach
looks as well.
map.forEach(4, this::someHeavyOperation);
In this example, the method will run in parallel when the size of the map reaches the parallelism threshold being 4.
Adding a transformer
There are also a versions of the forEach
where we can add a transformer.
map.forEach(1, (k, v) -> "There is " + v.size() + " articles about " + k, System.out::println);
The transformer transforms the data before sending it to the Consumer — pretty much like executing a map
function on the key-value pair before passing it along.
A powerful search through a map
Let's continue by looking at another method — search
.
The idea behind the search method is that you provide a search function to find a key-value pair.
The function you provide not only define what's a qualified key-value pair, but also the result that will be returned.
If the current key-value pair doesn't qualify, the provided function needs to return null
to make the search continue.
So what if no pair qualify? Well, then the search
will simply return null
.
Let's see how this actually works by searching for a tag that contains more than 10 articles.
map.search(1, (k, v) -> {
return v.size() > 10 ? return k : null;
});
The first parameter is the parallelism threshold that we’ve discussed earlier, while the next argument is the function describing our search.
The search function first check if the size of the list of articles is more then 10, then returns the tag if it qualifies.
Searching based on the keys
As with forEach
, there are many variants of search
.
searchKeys
is one variant — taking a function with only the key as a parameter.
map.searchKeys(1, key -> key.equals("Java") ? key : null);
Searching based on the values
We also got searchValues
— taking a function only with the value as a parameter.
To see how this works, let's create a search function that finds the first list of articles that is not empty, before returing the first element in this list.
map.searchValues(1, v -> !v.isEmpty() ? v.get(0) : null);
Accumulating the data in a map
reduce
is a great operator commonly used in most functional languages.
With the release of Java 8 — this also got widely available in different interfaces and classes in Java — including ConcurrentHashMap
.
It’s actually added 19 different variants of the reducer in ConcurrentHashMap
.
Let’s look at a few.
A standard reduce
Assuming that an article only appears in one of the lists — let’s use reduce to count how many articles our map contains.
map.reduce(100, (k, v) -> v.size(), (total, elem) -> total + elem);
Again, we see the parallelism threshold as a first parameter.
The next parameter is a transformer — also seen in earlier examples. In our example we use the transformer to map from a key-value pair, to the count of articles for that given tag.
The last function is the reducer
— combining all the resulting elements from the transformation using a BiFunction
.
In our case, it’ll add all the counts of articles together — resulting in the total number of articles in our map.
Reduce the keys
If you don't care about the values of the map, you could use reduceKeys
to only define a reducer for your keys.
There are two versions of reduceKeys
— one with a transformer and one version without.
Let's look at an example without a transformer, where we find the first tag based on the alphabetic order.
map.reduceKeys(1, (k1, k2) -> k1.compareTo(k2) < 0 ? k1 : k2);
Reduce the values
If keys are not what you're interested in — only the values —reduceValue
could be a nice fit.
As with reduceKeys
— reduceValues
comes in a version with a transformer and a version without.
This time let's create an example using a transformer, where we rewrite our first example — counting the number of articles in our map of tags.
map.reduceValues(1, List::size, (total, elem) -> total + elem);
Further reading
Make sure to checkout the documentation for the complete list of available methods in ConcurrentHashMap
.