如何使用 Java 8 Streams 对对象属性进行分组并映射到另一个对象?
How to groupBy object properties and map to another object using Java 8 Streams?
假设我有一组碰碰车,它们的侧面有尺寸、颜色和标识符(“汽车代码”)。
class BumperCar {
int size;
String color;
String carCode;
}
现在我需要将碰碰车映射到 List
个 DistGroup
对象,每个对象都包含属性 size
、color
和 List
汽车代码。
class DistGroup {
int size;
Color color;
List<String> carCodes;
void addCarCodes(List<String> carCodes) {
this.carCodes.addAll(carCodes);
}
}
例如,
[
BumperCar(size=3, color=yellow, carCode=Q4M),
BumperCar(size=3, color=yellow, carCode=T5A),
BumperCar(size=3, color=red, carCode=6NR)
]
应该导致:
[
DistGroup(size=3, color=yellow, carCodes=[ Q4M, T5A ]),
DistGroup(size=3, color=red, carCodes=[ 6NR ])
]
我尝试了以下方法,它实际上做了我想要它做的事情。但问题是它具体化了中间结果(变成Map
)我也认为它可以一次完成(也许使用mapping
或collectingAndThen
或reducing
或其他东西),从而产生更优雅的代码。
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());
如何在不使用变量作为中间结果的情况下获得所需的结果?
编辑: 如何在不具体化中间结果的情况下获得所需的结果?我只是在寻找一种不会实现中间结果的方法,至少在表面上不会。这意味着我不想使用这样的东西:
something.stream()
.collect(…) // Materializing
.stream()
.collect(…); // Materializing second time
当然,如果可以的话。
请注意,为简洁起见,我省略了 getter 和构造函数。您还可以假设 equals
和 hashCode
方法已正确实现。另请注意,我正在使用 SizeColorCombination
用作分组键。这个 class 显然包含属性 size
和 color
。 类 类似于 Tuple
、Pair
、Entry
或任何其他表示两个任意值组合的 class。
编辑:另请注意,当然可以使用 ol' skool for 循环,但这不在本问题的范围内。
解决方案一
只是将两个步骤合二为一:
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());
解决方案-2
如果您可以使用 groupingBy
两次使用属性并将值映射为 List
代码,您的中间变量会好得多,例如:
Map<Integer, Map<String, List<String>>> sizeGroupedData = bumperCars.stream()
.collect(Collectors.groupingBy(BumperCar::getSize,
Collectors.groupingBy(BumperCar::getColor,
Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))));
并简单地使用 forEach
添加到最终列表中:
List<DistGroup> distGroups = new ArrayList<>();
sizeGroupedData.forEach((size, colorGrouped) ->
colorGrouped.forEach((color, carCodes) -> distGroups.add(new DistGroup(size, color, carCodes))));
注意:我已经更新了您的构造函数,使其接受卡片代码列表。
DistGroup(int size, String color, List<String> carCodes) {
this.size = size;
this.color = color;
addCarCodes(carCodes);
}
将第二个解决方案进一步组合成一个完整的陈述(尽管老实说我自己更喜欢forEach
):
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());
如果我们假设 DistGroup
基于 size
和 color
有 hashCode/equals
,你可以这样做:
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>
您可以使用BiConsumer
以(HashMap<SizeColorCombination, DistGroup> res, BumperCar bc)
为参数进行收集
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();
看看我的图书馆 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();
我想到了另一个解决方案。
首先,我们稍微调整 DistGroup
class:
class DistGroup {
private int size;
private String color;
private List<String> carCodes;
private DistGroup(int size, String color, List<String> carCodes) {
this.size = size;
this.color = color;
this.carCodes = carCodes;
}
// So we can use a method reference nicely
public static DistGroup ofBumperCar(BumperCar car) {
return new DistGroup(car.size(), car.color(), new ArrayList<>(Arrays.asList(car.carCode())));
}
public DistGroup merge(DistGroup other) {
assert size == other.size;
assert Objects.equals(color, other.color);
this.carCodes.addAll(other.carCodes);
return this;
}
}
请注意,我删除了 addCarCodes
方法,因为不需要它。
现在我们实际上可以使用 Collectors::toMap
方法很容易地得到想要的结果。
Collection<DistGroup> dists = cars.stream()
.collect(Collectors.toMap(
car -> new SizeColorGroup(car.size(), car.color()),
DistGroup::ofBumperCar,
DistGroup::merge
))
.values();
假设我有一组碰碰车,它们的侧面有尺寸、颜色和标识符(“汽车代码”)。
class BumperCar {
int size;
String color;
String carCode;
}
现在我需要将碰碰车映射到 List
个 DistGroup
对象,每个对象都包含属性 size
、color
和 List
汽车代码。
class DistGroup {
int size;
Color color;
List<String> carCodes;
void addCarCodes(List<String> carCodes) {
this.carCodes.addAll(carCodes);
}
}
例如,
[
BumperCar(size=3, color=yellow, carCode=Q4M),
BumperCar(size=3, color=yellow, carCode=T5A),
BumperCar(size=3, color=red, carCode=6NR)
]
应该导致:
[
DistGroup(size=3, color=yellow, carCodes=[ Q4M, T5A ]),
DistGroup(size=3, color=red, carCodes=[ 6NR ])
]
我尝试了以下方法,它实际上做了我想要它做的事情。但问题是它具体化了中间结果(变成Map
)我也认为它可以一次完成(也许使用mapping
或collectingAndThen
或reducing
或其他东西),从而产生更优雅的代码。
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());
如何在不使用变量作为中间结果的情况下获得所需的结果?
编辑: 如何在不具体化中间结果的情况下获得所需的结果?我只是在寻找一种不会实现中间结果的方法,至少在表面上不会。这意味着我不想使用这样的东西:
something.stream()
.collect(…) // Materializing
.stream()
.collect(…); // Materializing second time
当然,如果可以的话。
请注意,为简洁起见,我省略了 getter 和构造函数。您还可以假设 equals
和 hashCode
方法已正确实现。另请注意,我正在使用 SizeColorCombination
用作分组键。这个 class 显然包含属性 size
和 color
。 类 类似于 Tuple
、Pair
、Entry
或任何其他表示两个任意值组合的 class。
编辑:另请注意,当然可以使用 ol' skool for 循环,但这不在本问题的范围内。
解决方案一
只是将两个步骤合二为一:
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());
解决方案-2
如果您可以使用 groupingBy
两次使用属性并将值映射为 List
代码,您的中间变量会好得多,例如:
Map<Integer, Map<String, List<String>>> sizeGroupedData = bumperCars.stream()
.collect(Collectors.groupingBy(BumperCar::getSize,
Collectors.groupingBy(BumperCar::getColor,
Collectors.mapping(BumperCar::getCarCode, Collectors.toList()))));
并简单地使用 forEach
添加到最终列表中:
List<DistGroup> distGroups = new ArrayList<>();
sizeGroupedData.forEach((size, colorGrouped) ->
colorGrouped.forEach((color, carCodes) -> distGroups.add(new DistGroup(size, color, carCodes))));
注意:我已经更新了您的构造函数,使其接受卡片代码列表。
DistGroup(int size, String color, List<String> carCodes) {
this.size = size;
this.color = color;
addCarCodes(carCodes);
}
将第二个解决方案进一步组合成一个完整的陈述(尽管老实说我自己更喜欢forEach
):
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());
如果我们假设 DistGroup
基于 size
和 color
有 hashCode/equals
,你可以这样做:
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>
您可以使用BiConsumer
以(HashMap<SizeColorCombination, DistGroup> res, BumperCar bc)
为参数进行收集
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();
看看我的图书馆 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();
我想到了另一个解决方案。
首先,我们稍微调整 DistGroup
class:
class DistGroup {
private int size;
private String color;
private List<String> carCodes;
private DistGroup(int size, String color, List<String> carCodes) {
this.size = size;
this.color = color;
this.carCodes = carCodes;
}
// So we can use a method reference nicely
public static DistGroup ofBumperCar(BumperCar car) {
return new DistGroup(car.size(), car.color(), new ArrayList<>(Arrays.asList(car.carCode())));
}
public DistGroup merge(DistGroup other) {
assert size == other.size;
assert Objects.equals(color, other.color);
this.carCodes.addAll(other.carCodes);
return this;
}
}
请注意,我删除了 addCarCodes
方法,因为不需要它。
现在我们实际上可以使用 Collectors::toMap
方法很容易地得到想要的结果。
Collection<DistGroup> dists = cars.stream()
.collect(Collectors.toMap(
car -> new SizeColorGroup(car.size(), car.color()),
DistGroup::ofBumperCar,
DistGroup::merge
))
.values();