21

I would like to convert my map which looks like this:

{
  key="someKey1", value=Apple(id="1", color="green"),
  key="someKey2", value=Apple(id="2", color="red"),
  key="someKey3", value=Apple(id="3", color="green"),
  key="someKey4", value=Apple(id="4", color="red"),
}

to another map which puts all apples of the same color into the same list:

{
  key="red", value=list={apple1, apple3},
  key="green", value=list={apple2, apple4},  
}

I tried the following:

Map<String, Set<Apple>> sortedApples = appleMap.entrySet()
    .stream()
    .collect(Collectors.toMap(l -> l.getColour, ???));

Am I on the right track? Should I use filters for this task? Is there an easier way?

4 Answers 4

14

Collectors.groupingBy is more suitable than Collectors.toMap for this task (though both can be used).

Map<String, List<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour));

Or, to group them into Sets use:

Map<String, Set<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour,
                                           Collectors.mapping(Function.identity(),
                                                              Collectors.toSet())));

or (as Aomine commented):

Map<String, Set<Apple>> sortedApples = 
    appleMap.values()
            .stream()
            .collect(Collectors.groupingBy(Apple::getColour, Collectors.toSet()));
Sign up to request clarification or add additional context in comments.

Comments

11

if you want to proceed with toMap you can get the result as follows:

map.values()  // get the apples
   .stream() // Stream<Apple>
   .collect(toMap(Apple::getColour, // group by colour
             v ->  new HashSet<>(singleton(v)), // have values as set of apples
          (l, r) -> {l.addAll(r); return l;})); // merge colliding apples by colour
  • stream over the map values instead of entrySet because we're not concerned with the map keys.
  • Apple::getColour is the keyMapper function used to extract the "thing" we wish to group by, in this case, the Apples colour.
  • v -> new HashSet<>(singleton(v)) is the valueMapper function used for the resulting map values
  • (l, r) -> {l.addAll(r); return l;} is the merge function used to combine two HashSet's when there is a key collision on the Apple's colour.
  • finally, the resulting map is a Map<String, Set<Apple>>

but this is better with groupingBy and toSet as downstream:

map.values().stream().collect(groupingBy(Apple::getColour, toSet()));
  • stream over the map values instead of entrySet because we're not concerned with the map keys.

  • groups the Apple's by the provided classification function i.e. Apple::getColour and then collect the values in a Set hence the toSet downstream collector.

  • finally, the resulting map is a Map<String, Set<Apple>>

short, readable and the idiomatic approach.

You could also do it without a stream:

Map<String, Set<Apple>> res = new HashMap<>();
map.values().forEach(a -> res.computeIfAbsent(a.getColour(), e -> new HashSet<>()).add(a));
  • iterate over the map values instead of entrySet because we're not concerned with the map keys.
  • if the specified key a.getColour() is not already associated with a value, attempts to compute its value using the given mapping function e -> new HashSet<>() and enters it into the map. we then add the Apple to the resulting set.
  • if the specified key a.getColour() is already associated with a value computeIfAbsent returns the existing value associated with it and then we call add(a) on the HashSet to enter the Apple into the set.
  • finally, the resulting map is a Map<String, Set<Apple>>

Comments

8

You can use Collectors.groupingBy and Collectors.toSet()

Map<String, Set<Apple>> sortedApples = appleMap.values() // Collection<Apple>
        .stream() // Stream<Apple>
        .collect(Collectors.groupingBy(Apple::getColour, // groupBy colour
                Collectors.mapping(a -> a, Collectors.toSet()))); // collect to Set

Comments

3

You've asked how to do it with streams, yet here's another way:

Map<String, Set<Apple>> result = new LinkedHashMap<>();
appleMap.values().forEach(apple -> 
    result.computeIfAbsent(apple.getColor(), k -> new LinkedHashSet<>()).add(apple));

This uses Map.computeIfAbsent, which either returns the set mapped to that color or puts an empty LinkedHashSet into the map if there's nothing mapped to that color yet, then adds the apple to the set.

EDIT: I'm using LinkedHashMap and LinkedHashSet to preserve insertion order, but could have used HashMap and HashSet, respectively.

1 Comment

nice! +1, it would be better to iterate over the map values though since you're not doing anything with the key, i.e. appleMap.values().forEach(apple -> result.computeIfAbsent(apple.getColour(), k -> new LinkedHashSet<>()).add(apple));

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.