问题
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.
回答1:
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>
回答2:
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());
回答3:
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();
回答4:
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();
来源:https://stackoverflow.com/questions/54254064/how-to-groupby-object-properties-and-map-to-another-object-using-java-8-streams