Search code examples
javajava-8java-streamgroupingcollectors

How to groupBy object properties and map to another object using Java 8 Streams?


Suppose I have a group of bumper cars, which have a size, a color and an identifier ("car code") on their sides.

class BumperCar {
    int size;
    String color;
    String carCode;
}

Now I need to map the bumper cars to a List of DistGroup objects, which each contains the properties size, color and a List of car codes.

class DistGroup {
    int size;
    Color color;
    List<String> carCodes;

    void addCarCodes(List<String> carCodes) {
        this.carCodes.addAll(carCodes);
    }
}

For example,

[
    BumperCar(size=3, color=yellow, carCode=Q4M),
    BumperCar(size=3, color=yellow, carCode=T5A),
    BumperCar(size=3, color=red, carCode=6NR)
]

should result in:

[
    DistGroup(size=3, color=yellow, carCodes=[ Q4M, T5A ]),
    DistGroup(size=3, color=red, carCodes=[ 6NR ])
]

I tried the following, which actually does what I want it to do. But the problem is that it materializes the intermediate result (into a Map) and I also think that it can be done at once (perhaps using mapping or collectingAndThen or reducing or something), resulting in more elegant code.

List<BumperCar> bumperCars = …;
Map<SizeColorCombination, List<BumperCar>> map = bumperCars.stream()
    .collect(groupingBy(t -> new SizeColorCombination(t.getSize(), t.getColor())));

List<DistGroup> distGroups = map.entrySet().stream()
    .map(t -> {
        DistGroup d = new DistGroup(t.getKey().getSize(), t.getKey().getColor());
        d.addCarCodes(t.getValue().stream()
            .map(BumperCar::getCarCode)
            .collect(toList()));
        return d;
    })
    .collect(toList());

How can I get the desired result without using a variable for an intermediate result?

Edit: How can I get the desired result without materializing the intermediate result? I am merely looking for a way which does not materialize the intermediate result, at least not on the surface. That means that I prefer not to use something like this:

something.stream()
    .collect(…) // Materializing
    .stream()
    .collect(…); // Materializing second time

Of course, if this is possible.


Note that I omitted getters and constructors for brevity. You may also assume that equals and hashCode methods are properly implemented. Also note that I'm using the SizeColorCombination which I use as group-by key. This class obviously contains the properties size and color. Classes like Tuple, Pair, Entry or any other class representing a combination of two arbitrary values may also be used.
Edit: Also note that an ol' skool for loop can be used instead, of course, but that is not in the scope of this question.


Solution

  • If we assume that DistGroup has hashCode/equals based on size and color, you could do it like this:

    bumperCars
        .stream()
        .map(x -> {
            List<String> list = new ArrayList<>();
            list.add(x.getCarCode());
            return new SimpleEntry<>(x, list);
        })
        .map(x -> new DistGroup(x.getKey().getSize(), x.getKey().getColor(), x.getValue()))
        .collect(Collectors.toMap(
            Function.identity(),
            Function.identity(),
            (left, right) -> {
                left.getCarCodes().addAll(right.getCarCodes());
                return left;
            }))
        .values(); // Collection<DistGroup>