1

Say I have a list of country objects that contain lists of languages spoken in that country, like so:

class Country {
    List<String> languages; 
}

I would like to create a map in the following format: Map<String, List<Country>>, such that a each language maps to a list of Country objects. E.g.:

"French" -> [Country:France, Country:Canada],
"English" -> [Country:UK, Country:US]

Performance is an issue here, so I would like to avoid multiple iterations and lookups. I have tried using groupingBy, but how do I flatMap the keyset?

Example, this results in Map<List<String>, List<Country>>:

countries.stream()
    .collect(Collectors.groupingBy(country -> country.getLanguages(), toList()));
Edwin
  • 33
  • 6

3 Answers3

1

Since you seem to care about performance, don't use streams for this simple task:

Map<String, List<Country>> countriesByLanguage = new HashMap<>();
for (Country country : countries) {
    for (String language : country.getLanguages()) {
        countriesByLanguage.computeIfAbsent(language, k -> new ArrayList<>())
                           .add(country);
    }
}
Andreas
  • 154,647
  • 11
  • 152
  • 247
  • Wouldn't the stream be faster if I call it a single time with a large dataset? And only slower if I call it many times, with small sets, because it has more overhead? – Edwin Mar 26 '19 at 15:25
  • 1
    @Edwin Why do you believe a stream is faster than a normal loop? The overhead of streams will make it slower. In this particular case, streaming requires creating intermediate objects (e.g. `SimpleEntry`), so causes generation of garbage, which is also a consideration. On top of that, I find the non-stream solution here to be simpler. So, just because you can use streams, doesn't mean you should. Streams are great, but they are not the answer to everything. – Andreas Mar 26 '19 at 15:30
  • Would you mind elaborating on the use of computeIfAbsent? What happens here if a key is present, how is it added to the list? – Edwin Mar 26 '19 at 15:55
  • 1
    @Edwin Why would I need to elaborate on that? Isn't the **documentation** enough? The [javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfAbsent-K-java.util.function.Function-)? – Andreas Mar 26 '19 at 15:58
  • I originally read it as: `countriesByLanguage.computeIfAbsent(language, k -> new ArrayList<>().add(country));` Which was fairly confusing, this is clearly the best solution though, thanks. – Edwin Mar 26 '19 at 16:14
1

You can do it using a stream in a stream as follows: first iterate over the list of countries, then iterate over the nested list of languages and prepare the «language, country» pairs, and then collect them to map:

public static void main(String[] args) {
    List<Country> countries = List.of(
            new Country("France", List.of("French")),
            new Country("Canada", List.of("French")),
            new Country("UK", List.of("English")),
            new Country("US", List.of("English")));

    Map<String, List<Country>> map = countries.stream()
            // Stream<Map.Entry<String,Country>>
            .flatMap(country -> country.getLanguages().stream()
                    .map(lang -> Map.entry(lang, country)))
            .collect(Collectors.toMap(
                    // key - language
                    Map.Entry::getKey,
                    // value - List<Country>
                    entry -> new ArrayList<>(List.of(entry.getValue())),
                    // merge duplicates, if any
                    (list1, list2) -> {
                        list1.addAll(list2);
                        return list1;
                    }
            ));

    // output
    map.forEach((k, v) -> System.out.println(k + "=" + v));
    //English=[Country:UK, Country:US]
    //French=[Country:France, Country:Canada]
}
static class Country {
    String name;
    List<String> languages;

    public Country(String name, List<String> languages) {
        this.name = name;
        this.languages = languages;
    }

    public List<String> getLanguages() {
        return languages;
    }

    @Override
    public String toString() {
        return "Country:" + name;
    }
}
0

This'll do it:

countries.stream()
        .flatMap(country -> country.getLanguages()
                .stream()
                .map(lang -> new SimpleEntry<>(lang,
                        new ArrayList<>(Arrays.asList(country)))))
        .collect(Collectors.toMap(
                Entry::getKey,
                Entry::getValue,
                (l1, l2) -> {
                    l1.addAll(l2);
                    return l2;
                }));
Community
  • 1
  • 1
Not a JD
  • 1,864
  • 6
  • 14