Java stream collect to map with multiple keys
Asked Answered
G

2

5

I thought I was getting pretty good at Java 8 streams, but then...

I have a Foo interface:

public interface Foo {
  String getKey();
  Stream<Bar> bars();
}

I know I can collect a Stream<Foo> into a Map<String, Foo> using the key of each:

Map<String, Foo> foosByKey = fooStream.collect(
    Collectors.toMap(Foo::getKey, Function.identity()));

But what if I want to collect them into a Map<Bar, Foo>? In other words, for each Foo in the steam, I want to put that Foo in the map keyed to every one of the Bar instances returned by Foo.bars(). Where do I start?

Germ answered 22/6, 2016 at 0:28 Comment(5)
No, you have it backwards. A Map<Bar, Foo> would imply a one-to-one mapping between Bar and Foo instances. Consider Map<CreditCardNumber, Person> or Map<ISBN, Book>, where a person can have multiple credit cards and a book can have multiple ISBNs. Nothing complicated here.Germ
I do understand the meaning, but I didn't want to get hung up on points that are a bit ancillary. Whether a mapping is bidirectional depends on whether the graph is directional. A Java Map is a unidirectional, so you can't map back---which is why Guava has a BiMap, for instance. I think you wanted to say that the relationship is one-to-one---in this case the relationship is many-to-one (Bar-Foo). But again this is all sort of beside the point---things to debate over a beer. The summary is that each Foo can have many Bar, and I want to map Bar to Foo. Cheers!Germ
Indeed, we can close with the statement that each Foo can have many Bar and that all Bars are distinct and, well, I think Sotirios’ answer contains an appropriate solution. Is there anything that stops you from accepting his answer, that we should address?Reg
I was hoping for something a little more elegant and concise; without explicit intermediaries. I'll hold out for a little longer and see if anyone comes up with any tricks.Germ
I tried with a specialized collector, then I realized that this not only is less readable, in the end it only replaces the explicit instantiation of Map.Entry instances with implicit instantiation of capturing lambda instances, so there is no improvement at all.Reg
E
9

As suggested here, you'll want extract the Bar values from each Foo and create pairs of them. Once you have the pairs, you can collect them into a Map. For example,

Map<Bar, Foo> map = fooStream.flatMap(foo -> foo.bars().map(bar -> new SimpleEntry<>(bar, foo)))
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); 

We use SimpleEntry here because it's available (Java doesn't have a simpler Pair type). You could write your own to be more specific.

Ephemera answered 22/6, 2016 at 0:28 Comment(2)
The SimpleEntry<> use is cool---I hadn't realized Java had something at least similar to a Pair. Overall though I was hoping for something a little more compact, but this may be the best we can get. I'll leave it for a while longer to see if we get any other options.Germ
Java 9 will add the possibility to use Map.entry(bar, foo) to construct value-based, immutable entries in a more concise way.Reg
C
1

You could define a new collector for that. One simple implementation (that always creates HashMap's of ArrayList's; no downstream support) could be:

public static <T, K>
Collector<T, ?, Map<K, List<T>>> multiGroupingBy(
        Function<? super T, Collection<? extends K>> multiClassifier) {
    return Collector.of(
            HashMap::new,
            (map, entry) -> {
                multiClassifier.apply(entry)
                        .forEach(
                                key -> map
                                        .computeIfAbsent(key,
                                                __ -> new ArrayList<>())
                                        .add(entry));
            },
            (map1, map2) -> {
                map2.forEach(
                        (key, list) -> map1
                                .computeIfAbsent(key,
                                        __ -> new ArrayList<>())
                                .addAll(list));
                return map1;
            });
}

Then you could call:

fooStream.collect(multiGroupingBy(Foo::bars));
Camper answered 16/3, 2020 at 17:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.