Anonymous
Anonymous

Reputation: 86262

How to group into map of arrays?

Can a groupingBy operation on a stream produce a map where the values are arrays rather than lists or some other collection type?

For example: I have a class Thing. Things have owners, so Thing has a getOwnerId method. In a stream of things I want to group the things by owner ID so that things with the same owner ID end up in an array together. In other words I want a map like the following where the keys are owner IDs and the values are arrays of things belonging to that owner.

    Map<String, Thing[]> mapOfArrays;

In my case, since I need to pass the map values to a library method that requires an array, it would be most convenient to collect into a Map<String, Thing[]>.

Collecting the whole stream into one array is easy (it doesn’t even require an explicit collector):

    Thing[] arrayOfThings = Stream.of(new Thing("owner1"), new Thing("owner2"), new Thing("owner1"))
            .toArray(Thing[]::new);

[Belongs to owner1, Belongs to owner2, Belongs to owner1]

Groping by owner ID is easy too. For example, to group into lists:

    Map<String, List<Thing>> mapOfLists = Stream.of(new Thing("owner1"), new Thing("owner2"), new Thing("owner1"))
            .collect(Collectors.groupingBy(Thing::getOwnerId));

{owner1=[Belongs to owner1, Belongs to owner1], owner2=[Belongs to owner2]}

Only this example gives me a map of lists. There are 2-arg and 3-arg groupingBy methods that can give me a map of other collection types (like sets). I figured, if I can pass a collector that collects into an array (similar to the collection into an array in the first snippet above) to the two-arg Collectors.groupingBy​(Function<? super T,? extends K>, Collector<? super T,A,D>), I’d be set. However, none of the predefined collectors in the Collectors class seem to do anything with arrays. Am I missing a not too complicated way through?

For the sake of a complete example, here’s the class I’ve used in the above snippets:

public class Thing {

    private String ownerId;

    public Thing(String ownerId) {
        this.ownerId = ownerId;
    }

    public String getOwnerId() {
        return ownerId;
    }

    @Override
    public String toString() {
        return "Belongs to " + ownerId;
    }

}

Upvotes: 3

Views: 1972

Answers (3)

Anonymous
Anonymous

Reputation: 86262

Using the collector from this answer by Thomas Pliakas:

    Map<String, Thing[]> mapOfArrays = Stream.of(new Thing("owner1"), new Thing("owner2"), new Thing("owner1"))
            .collect(Collectors.groupingBy(Thing::getOwnerId,
                    Collectors.collectingAndThen(Collectors.toList(),
                            tl -> tl.toArray(new Thing[0]))));

The idea is to collect into a list at first (which is an obvious idea since arrays have constant size) and then converting to an array before returning to the grouping by collector. collectingAndThen can do that through its so-called finisher.

To print the result for inspection:

    mapOfArrays.forEach((k, v) -> System.out.println(k + '=' + Arrays.toString(v)));
owner1=[Belongs to owner1, Belongs to owner1]
owner2=[Belongs to owner2]

Edit: With thanks to Aomine for the link: Using new Thing[0] as argument to toArray was inspired by Arrays of Wisdom of the Ancients. It seems that on Intel CPUs in the end using new Thing[0] is faster than using new Thing[tl.size()]. I was surprised.

Upvotes: 4

Eugene
Eugene

Reputation: 120848

Probably obvious but you could have done it via:

Stream.of(new Thing("owner1"), new Thing("owner2"), new Thing("owner1"))
            .collect(Collectors.toMap(
                    Thing::getOwnerId,
                    x -> new Thing[]{x},
                    (left, right) -> {
                        Thing[] newA = new Thing[left.length + right.length];
                        System.arraycopy(left, 0, newA, 0, left.length);
                        System.arraycopy(right, 0, newA, left.length, right.length);
                        return newA;
                    }
            ))

Upvotes: 2

Ousmane D.
Ousmane D.

Reputation: 56423

you could group first then use a subsequent toMap:

Map<String, Thing[]> result = source.stream()
                .collect(groupingBy(Thing::getOwnerId))
                .entrySet()
                .stream()
                .collect(toMap(Map.Entry::getKey,
                        e -> e.getValue().toArray(new Thing[0])));

Upvotes: 3

Related Questions