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

19
假设我有一组碰碰车,它们都有大小、颜色和一个标识符(“汽车代码”)在它们的侧面。
class BumperCar {
    int size;
    String color;
    String carCode;
}

现在我需要将碰碰车映射到一个 List 对象的 DistGroup 中,其中每个对象都包含 sizecolor 属性和一个汽车代码的 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)具体化了,并且我认为可以一次性完成(可能使用mappingcollectingAndThenreducing等方法),从而产生更优雅的代码。
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和constructor。您还可以假设equalshashCode方法已经正确实现。同时,请注意我使用SizeColorCombination作为分组键。这个类显然包含了sizecolor属性。也可以使用像TuplePairEntry或任何表示两个任意值组合的类。
编辑:还要注意,当然可以使用传统的for循环,但这不在本问题的范围内。

2
只是顺便提一下,groupingBy()默认将值分组为List,因此可以省略toList() - Lino
1
使用流的想法是使代码更易读,更自我解释(以性能为代价),或者在没有样板代码的情况下进行大规模并行化。它不是旧方法的现代一刀切替代品。您提供的代码充其量是晦涩难懂的。我建议在这种情况下使用经典的for循环,这样会更清晰。 - Mark Jeronimus
@Lino 你说得对。我已经把它移除了。 - MC Emperor
@MarkJeronimus 没错,这就是为什么我对当前的解决方案不满意,并正在寻找一种优雅的方式来实现完全相同的结果——如果存在的话。否则,我很乐意回到经典的循环。 - MC Emperor
5个回答

8
如果我们假设DistGrouphashCode/equals是基于sizecolor的,您可以这样做:
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>

谢谢,这段代码对我有用。请注意,我在使用这段代码后进行了一些调整,因此保险杠车直接映射到“DistGroup”,使用.map(t -> new DistGroup(t.getSize(), t.getColor(), new ArrayList <>(Arrays.asList(t.getCarCode()))))。然后,我使用与您的代码相同的参数将其收集到toMap中,但是第一个参数为t -> new SimpleEntry<>(t.getColor(), t.getSize())而不是Function.identity() - MC Emperor

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

如果您可以使用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());

0

我想出了另一种解决方案。

首先,我们稍微调整一下DistGroup类:

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

0

您可以通过使用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();

0

欢迎查看我的库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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接