0

I am trying to rewrite the method below using streams but I am not sure what the best approach is? If I use flatMap on the values of the entrySet(), I lose the reference to the current key.

private List<String> asList(final Map<String, List<String>> map) {
    final List<String> result = new ArrayList<>();
    for (final Entry<String, List<String>> entry : map.entrySet()) {
      final List<String> values = entry.getValue();
      values.forEach(value -> result.add(String.format("%s-%s", entry.getKey(), value)));
    }
    return result;
}

The best I managed to do is the following:

return map.keySet().stream()
          .flatMap(key -> map.get(key).stream()
                             .map(value -> new AbstractMap.SimpleEntry<>(key, value)))
          .map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
          .collect(Collectors.toList());

Is there a simpler way without resorting to creating new Entry objects?

2 Answers 2

2

A stream is a sequence of values (possibly unordered / parallel). map() is what you use when you want to map a single value in the sequence to some single other value. Say, map "alturkovic" to "ALTURKOVIC". flatMap() is what you use when you want to map a single value in the sequence to 0, 1, or many other values. Hence why a flatMap lambda needs to turn a value into a stream of values. flatMap can thus be used to take, say, a list of lists of string, and turn that into a stream of just strings.

Here, you want to map a single entry from your map (a single key/value pair) into a single element (a string describing it). 1 value to 1 value. That means flatMap is not appropriate. You're looking for just map.

Furthermore, you need both key and value to perform your mapping op, so, keySet() is also not appropriate. You're looking for entrySet(), which gives you a set of all k/v pairs, juts what we need.

That gets us to:

map.entrySet().stream()
   .map(e -> String.format("%s-%s", e.getKey(), e.getValue()))
   .collect(Collectors.toList());

Your original code makes no effort to treat a single value from a map (which is a List<String>) as separate values; you just call .toString() on the entire ordeal, and be done with it. This means the produced string looks like, say, [Hello, World] given a map value of List.of("Hello", "World"). If you don't want this, you still don't want flatmap, because streams are also homogenous - the values in a stream are all of the same kind, and thus a stream of 'key1 value1 value2 key2 valueA valueB' is not what you'd want:

map.entrySet().stream()
   .map(e -> String.format("%s-%s", e.getKey(), myPrint(e.getValue())))
   .collect(Collectors.toList());

public static String myPrint(List<String> in) {
   // write your own algorithm here
}

Stream API just isn't the right tool to replace that myPrint method.

A third alternative is that you want to smear out the map; you want each string in a mapvalue's List<String> to first be matched with the key (so that's re-stating that key rather a lot), and then do something to that. NOW flatMap IS appropriate - you want a stream of k/v pairs first, and then do something to that, and each element is now of the same kind. You want to turn the map:

key1 = [value1, value2]
key2 = [value3, value4]

first into a stream:

key1:value1
key1:value2
key2:value3
key2:value4

and take it from there. This explodes a single k/v entry in your map into more than one, thus, flatmapping needed:

return map.entrySet().stream()
    .flatMap(e -> e.getValue().stream()
      .map(v -> String.format("%s-%s", e.getKey(), v))
    .collect(Collectors.toList());

Going inside-out, it maps a single entry within a list that belongs to a single k/v pair into the string Key-SingleItemFromItsList.

Sign up to request clarification or add additional context in comments.

2 Comments

Amazing answer. I am not sure just about this statement: "Your original code makes no effort to treat a single value from a map (which is a List<String>) as separate values" The original code has a double loop iterating through single values without streams. The other example is also using both flatMap and map but inefficiently using map.get(key) instead of using entrySet(). Am I missing something there? What I was missing is the idea of using entrySet() and a combination of e.getKey() and a single value using map. Thank you very much!
The original code has a double loop iterating through single values without streams. - Yes, it does, my casual glance missed the use of .forEach. The third example, flatmapping into key1:value1, key1:value2, etc, most closely matches the intent of your code. Sorry about that!
0

Adding my two cents to excellent answer by @rzwitserloot. Already flatmap and map is explained in his answer.

List<String> resultLists =  myMap.entrySet().stream()
        .flatMap(mapEntry -> printEntries(mapEntry.getKey(),mapEntry.getValue())).collect(Collectors.toList());
System.out.println(resultLists);

Splitting this to a separate method gives good readability IMO,

private static Stream<String> printEntries(String key, List<String> values) {
    return values.stream().map(val -> String.format("%s-%s",key,val));
  }

Comments

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.