【问题标题】:How to groupBy object properties and map to another object using Java 8 Streams?如何使用 Java 8 Streams 对对象属性进行分组并映射到另一个对象?
【发布时间】:2019-01-18 12:29:25
【问题描述】:

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

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

现在我需要将碰碰车映射到DistGroup 对象中的List,每个对象都包含sizecolorList 的汽车代码属性。

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 和构造函数。您还可以假设 equalshashCode 方法已正确实现。另请注意,我使用的是SizeColorCombination,我将其用作分组键。这个类显然包含属性sizecolor。也可以使用 TuplePairEntry 等类或表示两个任意值组合的任何其他类。
编辑:另请注意,ol' skool for 循环当然可以使用,但这不在这个问题的范围内。

【问题讨论】:

  • 顺便说一句,groupingBy() 默认情况下会将值分组为List,因此可以省略toList()
  • 使用流的想法是使代码更具可读性、更不言自明(以性能为代价)或无需样板代码即可大规模并行化。这不是旧方法的现代万能解决方案。您提供的代码充其量是神秘的。我建议使用经典的 for 循环,在这种情况下更简洁。
  • @Lino 你是对的。我删除了它。
  • @MarkJeronimus 没错,这就是为什么我对当前的解决方案不满意并寻找一种优雅的方式来实现相同的结果——如果它存在的话。否则我会很乐意回到经典循环。

标签: java java-8 java-stream grouping collectors


【解决方案1】:

如果我们假设DistGroup 具有基于sizecolorhashCode/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>

【讨论】:

  • 谢谢,这段代码对我有用。请注意,在使用此代码后,我稍微调整了代码,因此碰碰车使用.map(t -&gt; new DistGroup(t.getSize(), t.getColor(), new ArrayList&lt;&gt;(Arrays.asList(t.getCarCode())))) 直接映射到DistGroup。然后我用toMap 收集它,参数与你的代码相同,但第一个参数是t -&gt; new SimpleEntry&lt;&gt;(t.getColor(), t.getSize()) 而不是Function.identity()
【解决方案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());

【讨论】:

    【解决方案3】:

    您可以使用以(HashMap&lt;SizeColorCombination, DistGroup&gt; res, BumperCar bc)为参数的BiConsumer进行收集

    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】:

      查看我的图书馆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();
      

      【讨论】:

        【解决方案5】:

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

        首先,我们稍微调整一下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();
        

        【讨论】:

          猜你喜欢
          • 2019-04-27
          • 2015-04-15
          • 2017-01-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-05-20
          相关资源
          最近更新 更多