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.
In Java 8, you retrieve the stream from the list and use a Collector to group them in one line of code. It's as simple as passing the grouping condition to the collector and it is complete. By simply modifying the grouping condition, you can create multiple groups.
With Java 8, you can convert a Map. entrySet() into a stream , follow by a filter() and collect() it.
Converting only the Value of the Map<Key, Value> into Stream: This can be done with the help of Map. values() method which returns a Set view of the values contained in this map. In Java 8, this returned set can be easily converted into a Stream of key-value pairs using Set. stream() method.
Collectors groupingBy() method in Java with Examples The groupingBy() method of Collectors class in Java are used for grouping objects by some property and storing results in a Map instance.
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>
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());
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());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With