Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confused by Java8 Collectors.toMap

I have a collection that looks like below, and I want to filter out the everything except the dates that aren't the end of the months.

2010-01-01=2100.00, 
2010-01-31=2108.74, 
2010-02-01=2208.74, 
2010-02-28=2217.92, 
2010-03-01=2317.92, 
2010-03-31=2327.57, 
2010-04-01=2427.57, 
2010-04-30=2437.67, 
2010-05-01=2537.67, 
2010-05-31=2548.22, 
2010-06-01=2648.22, 
2010-06-30=2659.24, 
2010-07-01=2759.24, 
2010-07-31=2770.72, 
2010-08-01=2870.72, 
2010-08-31=2882.66, 
2010-09-01=2982.66, 
2010-09-30=2995.07, 
2010-10-01=3095.07, 
2010-10-31=3107.94, 
2010-11-01=3207.94, 
2010-11-30=3221.29

I have the following filter criteria. frequency.getEnd returns a LocalDate matching the end of the month for the given LocalDate.

.filter(p -> frequency.getEnd(p.getKey()) == p.getKey())

So now I think I have to converted this filtered stream back to a map. And I think I use a collector to do that. Thus I add:

.collect(Collectors.toMap(/* HUH? */));

But I don't know what to do with Collectors.toMap. Reading examples leaves me confused. Here's my current code which obviously doesn't work.

TreeMap<LocalDate, BigDecimal> values = values.entrySet()
                                              .stream()
                                              .filter(p -> frequency.getEnd(p.getKey()) == p.getKey())
                                              .collect(Collectors.toMap(/* HUH? */));
like image 824
Patrick Avatar asked Nov 15 '15 20:11

Patrick


People also ask

How does collectors toMap work?

Collectors toMap() method in Java with Examples. The toMap() method is a static method of Collectors class which returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements.

Does collectors toList () return ArrayList?

For instance, Collectors. toList() method can return an ArrayList or a LinkedList or any other implementation of the List interface. To get the desired Collection, we can use the toCollection() method provided by the Collectors class.

What does collectors toList () do?

The toList() method of the Collectors class returns a Collector that accumulates the input elements into a new List.

What is the use of collectors in Java 8?

collect() is one of the Java 8's Stream API's terminal methods. It allows us to perform mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in a Stream instance.


4 Answers

Consider your problem like this: you have a Stream of entry of a map, that is to say a Stream<Map.Entry<LocalDate, BigDecimal>>, and you want to collect it into a TreeMap<LocalDate, BigDecimal>.

So, you are right, you should use Collectors.toMap. Now, as you can see in the documentation, there are actually 3 Collectors.toMap, depending on the arguments:

  • toMap(keyMapper, valueMapper). keyMapper is a function whose input is the stream current element and whose output is the key of the final Map. Thus, it maps the Stream element to a key (hence the name). valueMapper is a function whose input is the stream current element and whose output is the value of the final Map.
  • toMap(keyMapper, valueMapper, mergeFunction). The first two parameters are the same as before. The third, mergeFunction, is a function that is called in case of duplicates key elements in the final Map; therefore, its input are 2 values (i.e. the two values for which keyMapper returned the same key) and merges those two values into a single one.
  • toMap(keyMapper, valueMapper, mergeFunction, mapSupplier). The first three arguments are the same as before. The fourth is a supplier of a Map: as it's currently implemented in the JDK, the two preceding toMap return a HashMap instance. But if you want a specific Map instance, this supplier will return that instance.

In our specific case, we need to use the third toMap, because we want the result Map to explicitly be a TreeMap. Let's see what input we should give it:

  • keyMapper: so this should return the key of the final Map. Here, we are dealing with a Stream<Map.Entry<LocalDate, BigDecimal>> so each Stream element is of type Map.Entry<LocalDate, BigDecimal>. This function then takes a Map.Entry<LocalDate, BigDecimal> e as input. Its output should be the key of the final Map, in this case, the output should be e.getKey(), i.e. the LocalDate that the entry is holding. This can be written as a lambda expression: e -> e.getKey(). This could also be written as a method-reference Map.Entry::getKey but let's stick with lambdas here, because it might be easier to understand.
  • valueMapper: this is the same as above, but, in this case, this function needs to return e.getValue(), i.e. the BigDecimal that the entry is holding. So this is e -> e.getValue().
  • mergeFunction: this is a tricky one. We know that there are no duplicate key elements (i.e. no duplicate LocalDate) in the final Map, by construction. What do we write here? A simple solution is to throw an exception: this should not happen and if it does, there's a big problem somewhere. So whatever the two input arguments, we'll throw an exception. This can be written as (v1, v2) -> { throw new SomeException(); }. Note that it needs to be enclosed in brackets. In this case, and to be consistent with what the JDK currently does, I've chosen SomeException to be IllegalStateException.
  • mapSupplier: as said before, we want to supply a TreeMap. A supplier takes no argument and returns a new instance. So this can be written as () -> new TreeMap<>() here. Again, we could use a method-reference and write TreeMap::new.

Final code, where I've just written the collecting part of the Stream (note that in this code, you could also use the corresponding method-references, as said above, I added them here in comments):

Collectors.toMap(
       e -> e.getKey(),    // Map.Entry::getKey
       e -> e.getValue(),  // Map.Entry::getValue
       (v1, v2) -> { throw new IllegalStateException(); },
       () -> new TreeMap<>())  // TreeMap::new
)
like image 147
Tunaki Avatar answered Oct 14 '22 20:10

Tunaki


In addition to the previous answers note that if you don't need to keep the original map, you can perform such filtering in-place without using the Stream API:

values.keySet().removeIf(k -> !frequency.getEnd(k).equals(k));
like image 41
Tagir Valeev Avatar answered Oct 14 '22 18:10

Tagir Valeev


Since you're iterating Map.Entry values, and toMap() just needs two methods for extracting the key and the value, it's this simple:

Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)

Note that this will not return a TreeMap. For that, you need:

Collectors.toMap(Entry::getKey,
                 Entry::getValue,
                 (v1,v2) -> { throw new IllegalStateException("Duplicate key"); },
                 TreeMap::new)
like image 25
Andreas Avatar answered Oct 14 '22 19:10

Andreas


The toMap method in its simplest form takes two arguments: one is a function to map the input to the key, and the other a function to map the input to the value. The output of both functions is combined to form an entry in the resulting map.

I think you need to do something like this:

Collectors.toMap(p -> p.getKey(), p -> p.getValue())
like image 26
Tom Avatar answered Oct 14 '22 20:10

Tom