2

Is there a better way to transform "Map<String, Collection<String>>" to "Map<String, List<String>>"?

Map<String, Collection<String>> collectionsMap = ...
Map<String, List<String>> listsaps =
    collectionsMap.entrySet().stream()
    .collect(Collectors.<Map.Entry<String, Collection<String>>,
        String, List<String>>toMap(
            Map.Entry::getKey,
            e -> e. getValue().stream().collect(Collectors.toList())
        )
    );

Thank you for helping us improve

Stefan Zobel
  • 3,182
  • 7
  • 28
  • 38
Romain DEQUIDT
  • 792
  • 8
  • 15
  • 1
    I'm voting to close this question as off-topic because questions asking to review working code should go to https://codereview.stackexchange.com/ – GhostCat Feb 22 '18 at 13:41
  • 2
    @GhostCat That's what I always think but "How can I write X using Java 8" questions always seem to do very well. – Michael Feb 22 '18 at 13:45

3 Answers3

4

For cases like this, I'd consider using Map.forEach to perform the operation using side effects. Streams over maps are somewhat cumbersome, as one needs to write extra code to stream the map entries and then extract the key and value from each entry. By contrast, Map.forEach passes each key and value to the function as a separate parameter. Here's what that looks like:

Map<String, Collection<String>> collectionsMap = ...
Map<String, List<String>> listsaps = new HashMap<>(); // pre-size if desired
collectionsMap.forEach((k, v) -> listsaps.put(k, new ArrayList<>(v)));

If your map is large, you'll probably want to pre-size the destination in order to avoid rehashing during its population. To do this properly you have to know that HashMap takes the number of buckets, not the number of elements, as its parameter. This requires dividing by the default load factor of 0.75 in order to pre-size properly given a certain number of elements:

Map<String, List<String>> listsaps = new HashMap<>((int)(collectionsMap.size() / 0.75 + 1));
Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
  • good answer, as usual. I have a small question if I may about `(collectionsMap.size() / 0.75 + 1)`. I never understood this pattern, suppose that your `collectionsMap` has 16 buckets (for 0.75 load_factor) and 36 entries (3 per bucket for the 12 buckets that are actually used). This formula will created (49 the result, but because of next power of two) 64 buckets, 4 times more than the first one, I also understand that there is no way to query a map for it's number of buckets... – Eugene Mar 02 '18 at 08:39
  • @Eugene Thanks. No problem with questions. This area of HashMap is confusing and I always have to rethink it. It doesn't help that HashMap defines "capacity" as the number of buckets, which is pretty confusing. The load factor is the ratio between the number of buckets and the number of *entries* in the map. Note, *not* the number of occupied buckets. So, if there are 100 buckets, and the load factor is 0.75, up to 75 entries can be added to the map before the map decides to resize itself and rehash all the entries. This is true regardless of how many entries end up in each bucket. – Stuart Marks Mar 02 '18 at 22:10
  • @Eugene So, if we want to add N entries to a newly created map, and we want to avoid resizes while we're adding the entries, we have to create the HashMap with an "initial capacity" (number of buckets) greater than N / 0.75, assuming 0.75 as the default load factor. So if we want to populate a HashMap with 75 elements, we divide by 0.75 and add 1 to get 101 buckets. Now in fact this will get further rounded up to the next power of 2, which is 128, so resizing won't occur until 0.75*128=96 entries are added. – Stuart Marks Mar 02 '18 at 22:15
  • @Eugene Finally, note that ConcurrentHashMap's constructor that takes an "initial capacity" sizes the map according to that *number of elements* not the number of *buckets*. This makes sense for what people often want to do, which is to populate a map with N elements without resizing. However, it's confusing because it differs from HashMap. But I think that's HashMap's fault for being confusing in the first place. – Stuart Marks Mar 02 '18 at 22:16
  • oh my I feel like an idiot right now (1) thank you - this is good (2) I might need to search our code base for the things that I've erroneously introduced – Eugene Mar 04 '18 at 20:01
  • well... if you don't mind questions, would you have time for this also may be? https://stackoverflow.com/questions/45404580/hashmap-resize-method-implementation-detail – Eugene Mar 05 '18 at 09:18
3

1) In Collectors.toMap() you don't need to repeat the generic types as these are inferred.

So :

collect(Collectors.<Map.Entry<String, Collection<String>>,
        String, List<String>>toMap(...)

can be replaced by :

collect(Collectors.toMap(...)

2) The way of transforming the collection into a List could also be simplified.

This :

e -> e. getValue().stream().collect(Collectors.toList())

could be written as :

e -> new ArrayList<>(e.getValue())

You could write :

Map<String, List<String>> listsaps =
            collectionsMap.entrySet()
            .stream()
            .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    e -> new ArrayList<>(e.getValue())
                )
            );
davidxxx
  • 125,838
  • 23
  • 214
  • 215
1

I think that this is easier to read:

Map<String, List<String>> listsaps = new HashMap<>();
collectionsMap.entrySet()
    .stream()
    .forEach(e -> listsaps.put(e.getKey(), new ArrayList<>(e.getValue())));

If you just want to convert the entries to lists but don't really care about changing the type of the collection then you can use map.replaceAll:

collectionsMap.replaceAll((k, v) -> new ArrayList<>(v));
Michael
  • 41,989
  • 11
  • 82
  • 128
  • 1
    As @Klitos said, the replceAll signature is: void replaceAll(BiFunction super K, ? super V, ? extends V> function) So, I cannot get the result as a returned value. – Romain DEQUIDT Feb 22 '18 at 14:03
  • @RomainDEQUIDT The point of the second example is not to create a new map, but to replace the collections in the existing map in place. – Michael Feb 23 '18 at 11:07