Diogo Barroso
Diogo Barroso

Reputation: 11

Convert Map<Integer, List<Strings> to Map<String, List<Integer>

I'm having a hard time converting a Map containing some integers as keys and a list of random strings as values.

E.g. :

1 = ["a", "b", "c"]
2 = ["a", "b", "z"]
3 = ["z"]

I want to transform it into a Map of distinct strings as keys and lists the integers as values.

E.g. :

a = [1, 2]
b = [1, 2]
c = [1]
z = [2,3]

Here's what I have so far:

Map<Integer, List<String>> integerListMap; // initial list is already populated

List<String> distinctStrings = new ArrayList<>();
SortedMap<String, List<Integer>> stringListSortedMap = new TreeMap<>();

for(Integer i: integers) {
    integerListMap.put(i, strings);
    distinctStrings.addAll(strings);
}

distinctStrings = distinctStrings.stream().distinct().collect(Collectors.toList());

for(String s : distinctStrings) {
    distinctStrings.put(s, ???);
}

Upvotes: 0

Views: 856

Answers (2)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29038

There's no need to apply distinct() since you're storing the data into the Map and keys are guaranteed to be unique.

You can flatten the entries of the source map, so that only one string (let's call it name) and a single integer (let's call it number) would correspond to a stream element, and then group the data by string.

To implement this problem using streams, we can utilize flatMap() operation to perform one-to-many transformation. And it's a good practice to define a custom type for that purpose as a Java 16 record, or a class (you can also use a Map.Entry, but note that approach of using a custom type is more advantages because it allows writing self-documenting code).

In order to collect the data into a TreeMap you can make use of the three-args version of groupingBy() which allows to specify mapFactory.

record NameNumber(String name, Integer number) {}
        
Map<Integer, List<String>> dataByProvider = Map.of(
    1, List.of("a", "b", "c"),
    2, List.of("a", "b", "z"),
    3, List.of("z")
);
        
NavigableMap<String, List<Integer>> numbersByName = dataByProvider.entrySet().stream()
    .flatMap(entry -> entry.getValue().stream()
        .map(name -> new NameNumber(name, entry.getKey()))
    )
    .collect(Collectors.groupingBy(
        NameNumber::name,
        TreeMap::new,
        Collectors.mapping(NameNumber::number, Collectors.toList())
    ));
    
numbersByName.forEach((name, numbers) -> System.out.println(name + " -> " + numbers));

Output:

a -> [2, 1]
b -> [2, 1]
c -> [1]
z -> [3, 2]

Sidenote: while using TreeMap it's more beneficial to use NavigableMap as an abstract type because it allows to access methods like higherKey(), lowerKey(), firstEntry(), lastEntry(), etc. which are declared in the SortedMap interface.

Upvotes: 2

knittl
knittl

Reputation: 265648

Iterate over your source map's value and put each value into the target map.

final Map<String, List<Integer>> target = new HashMap<>();
for (final Map.Entry<Integer, List<String>> entry = source.entrySet()) {
  for (final String s : entry.getValue()) {
    target.computeIfAbsent(s, k -> new ArrayList<>()).add(entry.getKey());
  }
}

Or with the Stream API by abusing Map.Entry to build the inverse:

final Map<String, List<Integer>> target = source.entrySet()
  .stream()
  .flatMap(e -> e.getValue().stream().map(s -> Map.entry(s, e.getKey()))
  .collect(Collectors.groupingBy(e::getKey, Collectors.mapping(e::getValue, Collectors.toList())));

Although this might not be as clear as introducing a new custom type to hold the inverted mapping.

Another alternative would be using a bidirectial map. Many libraries come implementations of these, such as Guava.

Upvotes: 2

Related Questions