如何使用Java 8 Streams对By对象属性进行分组并映射到另一个对象?

2022-09-02 13:57:01

假设我有一组保险杠汽车,它们的侧面有尺寸,颜色和标识符(“汽车代码”)。

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

现在,我需要将保险杠汽车映射到一个对象,每个对象都包含属性 ,以及汽车代码的a。ListDistGroupsizecolorList

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 ])
]

我尝试了以下方法,这实际上做了我想要它做的事情。但问题是,它将中间结果具体化(变成一个),我也认为它可以立即完成(也许使用or或或什么),从而产生更优雅的代码。MapmappingcollectingAndThenreducing

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 和构造函数。您还可以假定 equalshashCode 方法已正确实现。另请注意,我使用的是 SizeColorCombination,我将其用作分组依据键。此类显然包含属性大小颜色。也可以使用元组PairEntry或任何其他表示两个任意值组合的类。
编辑:还要注意,当然,可以使用ol'skool for loop代替,但这不在本问题的范围之内。


答案 1

如果我们假设它基于 和 ,你可以这样做:DistGrouphashCode/equalssizecolor

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

解决方案-1

只需将两个步骤合并为一个:

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

如果您可以使用两次属性并将值映射到代码中,那么中间变量会更好,如下所示:groupingByList

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());