Java 8 流分组方式:对属性值求和

Fab*_* B. 5 java grouping java-8 java-stream

我有一个简单的类 Foo:

class Foo {
   String type; 
   Integer quantity;
   String code;
   //...
}
Run Code Online (Sandbox Code Playgroud)

以及该类型的对象列表:

{ type=11, quantity=1, code=A },
{ type=11, quantity=2, code=A },
{ type=11, quantity=1, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=11, quantity=1, code=B },
{ type=13, quantity=1, code=C },
{ type=13, quantity=3, code=C }
Run Code Online (Sandbox Code Playgroud)

我需要先按类型分组,我可以这样做:

Map<String, List<Foo>> typeGroups = mylist.stream().collect(Collectors.groupingBy(Foo::getType));
Run Code Online (Sandbox Code Playgroud)

下一步是将其转换MapList<Foo>这样的:

{ type=11, quantity=3, code=A },
{ type=11, quantity=2, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=13, quantity=4, code=C }
Run Code Online (Sandbox Code Playgroud)

在 SQL 中,我有这样的查询:

SELECT type, code, SUM(quantity) FROM Foo GROUP BY type, code
Run Code Online (Sandbox Code Playgroud)

让我也用简单的英语解释一下。

我需要得到一个 Foos 列表。每个人的代码在其组中必须是唯一的,数量必须是其组中具有相同代码的 Foo 的总和。

我尝试使用groupingBy()withsummingInt()但无法获得所需的结果。

Tun*_*aki 5

您需要使用groupingBy根据type和 制作的列表进行分组的收集器code。这是有效的,因为当两个列表的所有元素都相等且顺序相同时,两个列表就相等。(请注意,此处的列表仅用作值的容器,其相等性定义为包含的所有值相等且顺序相同;自定义类Tuple也可以在此处使用)。

以下代码首先创建一个中间映射Map<List<String>, Integer>,其中键对应于类型和代码的列表,值对应于这些类型和代码的所有数量的总和。然后对该映射进行后处理:每个条目都映射到 a,Foo其中type是键的第一个值, 是键的quantity值,code是键的第二个值。

Map<List<String>, Integer> map = 
    myList.stream()
          .collect(Collectors.groupingBy(
             f -> Arrays.asList(f.getType(), f.getCode()),
             Collectors.summingInt(Foo::getQuantity)
          ));

List<Foo> result = 
    map.entrySet()
       .stream()
       .map(e -> new Foo(e.getKey().get(0), e.getValue(), e.getKey().get(1)))
       .collect(Collectors.toList());
Run Code Online (Sandbox Code Playgroud)

请注意,上述解决方案使用了 2 个不同的 Stream 管道。这可以使用单个 Stream 管道来完成,但需要一个能够收集到 2 个不同收集器中的收集器。在这种情况下,第一个收集器将仅保留第一个分组值,第二个收集器将对数量求和。不幸的是,Stream API 本身没有这样的收集器。这可以使用包含此类收集器的StreamEx库来完成。

在下面的代码中,输入列表仍然按 的类型和代码形成的列表进行分组Foo。改变的是收集器:我们使用pairing(c1, c2, finisher)配对两个收集器并对收集器的结果应用给定的完成器操作。

  • 这里的第一个收集器只是first()返回第一个分组Foo(我们不需要其他收集器,因为我们知道它们将具有相同的类型和代码)。它包含一个调用,以从收集器中collectingAndThen提取值(返回 an来处理流为空的情况并将收集到一个空中;这在这里是不可能的,因此我们可以安全地调用)。Optionalfirst()first()OptionalOptionalget()
  • 第二个收集器只是将所有数量与 相加summingInt
  • 这里的整理器操作将Foo第一个收集器返回的结果与求和的结果组合成一个新的Foo具有新数量的结果。

最后,我们只需要保留结果图的值。由于Map.values()返回 a Collection,因此将其包装到 a 中ArrayList以获得最终结果。

List<Foo> result = new ArrayList<>(
    myList.stream()
          .collect(Collectors.groupingBy(
              f -> Arrays.<String>asList(f.getType(), f.getCode()),
              MoreCollectors.pairing(
                 Collectors.collectingAndThen(MoreCollectors.first(), Optional::get),
                 Collectors.summingInt(Foo::getQuantity),
                 (f, s) -> new Foo(f.getType(), s, f.getCode())
              )
          )).values());
Run Code Online (Sandbox Code Playgroud)

  • @Lii:“SimpleEntry”无法扩展以支持组的两个以上键,并且具有误导性的预定义语义。 (3认同)