Using groupingBy on a nested map, but collecting into a different object type

So, I have this code that "works" (replacing a few names for simplicity):

 Map<String, Map<String, ImmutableList<SomeClassA>>> someMap =
      someListOfClassA.stream()
      .filter(...)
      .collect(Collectors.groupingBy(SomeClassA::someCriteriaA,
            Collectors.groupingBy(SomeClassA::someCriteriaB, GuavaCollectors.toImmutableList()
            )
      ));

      

However, I want to change this code so that the inner collection is from SomeClassB after grouping by SomeClassA fields. For example, if the classes look like this:

assuming they both have all args constructors

class SomeClassA { 
    String someCriteriaA;
    String someCriteriaB;
    T someData;
    String someId;
}

class SomeClassB {
    T someData;
    String someId; 
}

      

And there is a way somewhere:

public static Collection<SomeClassB> getSomeClassBsFromSomeClassA(SomeClassA someA) {
    List<Some List of Class B> listOfB = someMethod(someA);
    return listOfB; // calls something using someClassA, gets a list of SomeClassB 
}

      

I want to flatten the resulting lists of SomeClass Bs in

Map<String, Map<String, ImmutableList<SomeClassB>>> someMap = 
    someListOfClassA.stream()
    .filter(...)
    . // not sure how to group by SomeClassA fields but result in a list of SomeClassB since one SomeClassA can result in multiple SomeClassB

      

I'm not sure how this would fit into the code above. How can I collect a bunch of lists based on SomeClassB into one list for all values โ€‹โ€‹of SomeClassA? If one ClassA maps to one ClassB, I know how to make it work using Collectors.mapping, but since each ClassA results in multiple ClassBs, I'm not sure how to get it to work.

Any ideas would be appreciated. Thank you!

+3


source to share


2 answers


With a custom collector, for example:

private static Collector<Collection<SomeClassB>, ?, ImmutableList<SomeClassB>>
        flatMapToImmutableList() {
        return Collectors.collectingAndThen(Collectors.toList(),
                listOfCollectionsOfB ->
                        listOfCollectionsOfB.stream()
                                .flatMap(Collection::stream)
                                .collect(GuavaCollectors.toImmutableList()));
    }

      



you can achieve what you need:

Map<String, Map<String, List<SomeClassB>>> someMap =
                someListOfClassA.stream()
                        .filter(...)
                        .collect(Collectors.groupingBy(SomeClassA::getSomeCriteriaA,
                                Collectors.groupingBy(SomeClassA::getSomeCriteriaB,
                                        Collectors.mapping(a -> getSomeClassBsFromSomeClassA(a),
                                                flatMapToImmutableList()))));

      

+4


source


While we're all waiting for Java 9 Collectors.flatMapping

(thanks @shmosel for the link), you can write your own collector to achieve what you want:

public static <T, D, R> Collector<T, ?, R> flatMapping(
        Function<? super T, ? extends Stream<? extends D>> streamMapper,
        Collector<? super D, ?, R> downstream) {

    class Acc {
        Stream.Builder<Stream<? extends D>> builder = Stream.builder();

        void add(T t) {
            builder.accept(streamMapper.apply(t));
        }

        Acc combine(Acc another) {
            another.builder.build().forEach(builder);
            return this;
        }

        R finish() {
            return builder.build()
                    .flatMap(Function.identity()) // Here!
                    .collect(downstream);
        }
    }
    return Collector.of(Acc::new, Acc::add, Acc::combine, Acc::finish);
}

      

This helper also uses a Collector.of

local class Acc

to accumulate the streams returned by the supplied function, streamMapper

which takes an element of the original stream as an argument. These flows are accumulated in Stream.Builder

, which will be created when using the Collector Finisher function.

Immediately after creating a stream of streams, it is densely mapped using the identity function, since we only want to concatenate streams. (I could use a list of streams instead of a stream of streams, but I think that Stream.Builder

is very efficient and heavily underused.)

Acc

also implements a combiner method that will combine a stream of streams Acc

into this stream builder Acc

. This function will only be used if the original stream is parallel.



Here's how to use this method in your example:

Map<String, Map<String, ImmutableList<SomeClassB>>> map = someListOfClassA.stream()
    .filter(...)
    .collect(
        Collectors.groupingBy(SomeClassA::getSomeCriteriaA,
            Collectors.groupingBy(SomeClassA::getSomeCriteriaB,
                flatMapping(
                    a -> getSomeClassBsFromSomeClassA(a).stream(),
                    ImmutableList.toImmutableList()))));

      


EDIT: As @Holger points out in the comments below, there is no need to buffer data in the stream builder while accumulating. Instead, a flat map collector can be implemented by performing right alignment as a battery function. Here is your own implementation of such a collector @Holger , which I copy here verbatim with his consent:

public static <T, U, A, R> Collector<T, ?, R> flatMapping(
        Function<? super T, ? extends Stream<? extends U>> mapper,
        Collector<? super U, A, R> downstream) {

    BiConsumer<A, ? super U> acc = downstream.accumulator();
    return Collector.of(downstream.supplier(),
            (a, t) -> {
                try (Stream<? extends U> s = mapper.apply(t)) {
                    if (s != null) s.forEachOrdered(u -> acc.accept(a, u));
                }
            },
            downstream.combiner(), downstream.finisher(),
            downstream.characteristics().toArray(new Collector.Characteristics[0]));
}

      

+3


source







All Articles