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

java 8 multi level grouping
java 8 group by multiple fields
java 8 group by and reduce
java8 list to map group by
java stream groupingby map value
java stream group by max
java 8 stream collect to map of lists
collectors.groupingby sorted map

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 immediate 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 immediate result?

Edit: How can I get the desired result without materializing the immediate result? I am merely looking for a way which does not materialize the immediate 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 or Pair 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.

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>

Guide to Java 8 groupingBy Collector, The Java 8 Stream API lets us process collections of data in a provide us with functionality similar to the 'GROUP BY' clause in the SQL language. They are used for grouping objects by some property and storing results in a Map instance. A different application of the downstream collector is to do a  The Java 8 Stream API lets us process collections of data in a declarative way. The static factory methods Collectors.groupingBy () and Collectors.groupingByConcurrent () provide us with functionality similar to the ‘ GROUP BY' clause in the SQL language. They are used for grouping objects by some property and storing results in a Map instance.

Solution-1

Just merging the two steps into one:

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

Your intermediate variable would be much better if you could use groupingBy twice using both the attributes and map the values as List of codes, something like:

Map<Integer, Map<String, List<String>>> sizeGroupedData = bumperCars.stream()
        .collect(Collectors.groupingBy(BumperCar::getSize,
                Collectors.groupingBy(BumperCar::getColor,
                        Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))));

and simply use forEach to add to the final list as:

List<DistGroup> distGroups = new ArrayList<>();
sizeGroupedData.forEach((size, colorGrouped) ->
        colorGrouped.forEach((color, carCodes) -> distGroups.add(new DistGroup(size, color, carCodes))));

Note: I've updated your constructor such that it accepts the card codes list.

DistGroup(int size, String color, List<String> carCodes) {
    this.size = size;
    this.color = color;
    addCarCodes(carCodes);
}

Further combining the second solution into one complete statement(though I would myself favor the forEach honestly):

List<DistGroup> distGroups = bumperCars.stream()
        .collect(Collectors.groupingBy(BumperCar::getSize,
                Collectors.groupingBy(BumperCar::getColor,
                        Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))))
        .entrySet()
        .stream()
        .flatMap(a -> a.getValue().entrySet()
                .stream().map(b -> new DistGroup(a.getKey(), b.getKey(), b.getValue())))
        .collect(Collectors.toList());

Java 8 – Stream Collectors groupingBy examples, Examples to 'group by' a list of user defined Objects. 2.1 A Pojo. Item.java. package com.mkyong.java8;  Given that I only need the Zipper objects to invoke the zip() method on them, is there a way to invoke this method as part of the stream just after the creation of each object (and to have these objects thrown away immediately after the zip() method has been invoked on them) rather than first having to create a map?

You can collect by by using BiConsumer that take (HashMap<SizeColorCombination, DistGroup> res, BumperCar bc) as parameters

Collection<DistGroup> values = bumperCars.stream()
        .collect(HashMap::new, (HashMap<SizeColorCombination, DistGroup> res, BumperCar bc) -> {
                SizeColorCombination dg = new SizeColorCombination(bc.color, bc.size);
                DistGroup distGroup = res.get(dg);
                if(distGroup != null) {
                    distGroup.addCarCode(bc.carCode);
                }else {
                    List<String> codes = new ArrayList();
                    distGroup = new DistGroup(bc.size, bc.color, codes);
                    res.put(dg, distGroup);
                }
                },HashMap::putAll).values();

Java 8 Stream grouping, Here we will cover java 8 stream group by on single field and java 8 Java 8 simplified the grouping of objects in the collection based one or more property values using to a classification function, and returning the results in a Map. Create simple Person pojo class with different attributes such as  Using a powerful API – Java 8 Stream Map; Now let’s do details with examples! Related posts: – Java 8 – Java 8 Streams. Java Transform a List with Traditional Solution by Looping Examples. Before Java 8, for mapping a List Objects, we use the basic approach with looping technical solution.

Check out my library AbacusUtil:

StreamEx.of(bumperCars)
         .groupBy(c -> Tuple.of(c.getSize(), c.getColor()), BumperCar::getCarCode)
         .map(e -> new DistGroup(e.getKey()._1, e.getKey()._2, e.getValue())
         .toList();

How to do GROUP BY in Java 8? Collectors , You can use Stream and Collector which provides groupingBy() In this Java 8 tutorial, you will learn how to group by a list of objects Here is our sample program to group objects on their properties in Java 8 and earlier version. list and put that item on the list, but when the group already exists in Map  Java 8 stream distinct by multiple fields By Lokesh Gupta | Filed Under: Java 8 Learn to collect distinct objects from a stream where each object is distinct by comparing multiple fields or properties in Java 8 .

The Ultimate Guide to the Java Stream API groupingBy() Collector , This post looks at using groupingBy() collectors with Java Stream APIs, focusing on the specific use cases, like custom Maps, downstream to SQL's GROUP BY clause, only it is for the Java Stream API. In order to use it, we always need to specify a property by which the grouping would be performed. In this article, we will show you how to use Java 8 Stream Collectors to group by, count, sum and sort a List.. 1. Group By, Count and Sort. 1.1 Group by a List and display the total count of it.

Java Streams groupingBy Examples, Using Java Streams and Collectors is a good way to implement SQL functionality to your aggregations so Well, with Java 8 streams operations, you are covered for some of these. Group by a Single Field The result is a map of year and players list grouped by the year. The value is the Player object. In Java 8, stream().map() lets you convert an object to something else. Review the following examples : 1. A List of Strings to Uppercase. 1.1 Simple Java example to convert a list of Strings to upper case.

Java 8 Grouping with Collectors, Tutorial on Java 8 groupingBy Collector with examples explains 3 This concept of grouping is the same as the 'group by' clause in SQL which takes an As the grouping collector works on the stream of objects its collecting from it of classification function on the stream elements, till the creation of Map  Find object from arraylist of objects using lambda stream java 8 (example) Sort objects on multiple fields /properties – Comparator interface (lambda stream java 8) Convert one/ heterogeneous object into another – stream lambda (java8 /example) Filter/Remove null & empty string from array – lambda stream java8 (example)

Comments
  • Just as a side note, groupingBy() does group the values into a List by default so toList() may be omitted
  • The idea of using streams is to make code more readable, more self-explanatory (at the cost of performance), or massively parallelizable without boilerplate code. It's not a modern one-solution-fits-all replacement of the old ways. The code you provided is cryptic at best. I suggest using a classic for-loop which is much cleaner in this case.
  • @Lino You're right. I removed it.
  • @MarkJeronimus That's right, that's why I'm not satisfied with my current solution and looking for an elegant way to achieve just the same result—if it exists. Otherwise I will gladly revert to the classic loop.
  • Thanks, this code works for me. Note that I, after using this code, have tweaked the code a little, so the bumper cars are directly mapped to a DistGroup using .map(t -> new DistGroup(t.getSize(), t.getColor(), new ArrayList<>(Arrays.asList(t.getCarCode())))). Then I collect it with toMap with the same arguments as your code, but with the first argument being t -> new SimpleEntry<>(t.getColor(), t.getSize()) instead of Function.identity().