【问题标题】:More than 2 groupingBy operations in Java 8Java 8 中超过 2 个 groupingBy 操作
【发布时间】:2019-08-28 10:08:51
【问题描述】:

这是我的“revenue_data.csv”文件:

Client  ReportDate  Revenue
C1      2019-1-7    12
C2      2019-1-7    34
C1      2019-1-16   56
C2      2019-1-16   78
C3      2019-1-16   90

而我读取文件的案例类是:

package com.source.code;

import java.time.LocalDate;

public class RevenueRecorder {

    private String clientCode;
    private LocalDate reportDate;
    private int revenue;


    public RevenueRecorder(String clientCode, LocalDate reportDate, int revenue) {
        this.clientCode = clientCode;
        this.reportDate = reportDate;
        this.revenue = revenue;
    }

    public String getClientCode() {
        return clientCode;
    }

    public LocalDate getReportDate() {
        return reportDate;
    }

    public int getRevenue() {
        return revenue;
    }
}

我可以通过以下方式按 ReportDate, sum(revenue) 读取文件和组:

import com.source.code.RevenueRecorder;

import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.summingInt;

public class RevenueRecorderMain {

    public static void main(String[] args) throws IOException {

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d");
        List<RevenueRecorder> revenueRecords = new ArrayList<>();

        Path path = FileSystems.getDefault().getPath("src", "main", "resources",
                "data", "revenue_data.csv");

        Files.lines(path)
                .skip(1)
                .map(s -> s.split(","))
                .forEach(s ->
                {
                    String clientCode = s[0];
                    LocalDate reportDate = LocalDate.parse(s[1], formatter);
                    int revenue = Integer.parseInt(s[2]);

                    revenueRecords.add(new RevenueRecorder(clientCode, reportDate, revenue));

                });

        Map<LocalDate, Integer> reportDateRev = revenueRecords.stream()
                .collect(groupingBy(RevenueRecorder::getReportDate,
                        summingInt(RevenueRecorder::getRevenue)));
    }
}

我的问题是如何在 Java 8 中按 ReportDate、count(clientCode) 和 sum(revenue) 进行分组,具体来说:

  • 使用什么集合代替地图
  • 在这种情况下如何进行分组和收集(通常用于 2 个以上的分组)

我正在尝试:

//import org.apache.commons.lang3.tuple.ImmutablePair;
//import org.apache.commons.lang3.tuple.Pair;

        Map<LocalDate, Pair<Integer, Integer>> pairedReportDateRev = revenueRecords.stream()
                .collect(groupingBy(RevenueRecorder::getReportDate,
                        new ImmutablePair(summingInt(RevenueRecorder::getRevenue),
                        groupingBy(RevenueRecorder::getClientCode, Collectors.counting()))));

但是在 RevenueRecorder::getReportDate 下获取 Intellij red-squiggle 并带有悬停消息“不能从静态上下文引用非静态方法”。

谢谢

编辑 为澄清起见,这是我试图获取的相应 SQL 查询:

select 
    reportDate, count(distinct(clientCode)), sum(revenue)
from
    revenue_data_table
group by
    reportDate

【问题讨论】:

  • 可能更容易创建自己的聚合类并收集到其中。
  • C1C2C3 等客户端代码如何映射到 Integer?多个客户端代码应该如何组合成一个Pair
  • Map&lt;LocalDate, Pair&lt;Integer, Integer&gt;&gt; pairedReportDateMRR = revenueRecords.stream() .collect(Collectors.toMap(RevenueRecorder::getReportDate, v -&gt; new Pair&lt;&gt;(1, v.getRevenue()), (p1, p2) -&gt; new Pair&lt;&gt;(p1.getKey() + p2.getKey(), p1.getValue() + p2.getValue())));我没有测试过。
  • 当您按客户端键分组时,您会得到包含客户端键的结果,即从客户端键到值的Map 映射,而不是整数。
  • @Hadi:你的解决方案有效,只需要用具体的实现来改变抽象的 Pair(),谢谢;

标签: java java-8 java-stream grouping


【解决方案1】:

虽然你的尝试没有成功,但我想是你最想表达的。所以我只是按照您的代码进行修复。试试这个!

Map<LocalDate, ImmutablePair<Integer, Map<String, Long>>> map = revenueRecords.stream()
            .collect(groupingBy(RevenueRecorder::getReportDate,
                    collectingAndThen(toList(), list -> new ImmutablePair(list.stream().collect(summingInt(RevenueRecorder::getRevenue)),
                                                                          list.stream().collect(groupingBy(RevenueRecorder::getClientCode, Collectors.counting()))))));

我从@Lyashko Kirill 那里借了一些示例数据代码来测试我的代码,结果如下

这是我自己的想法,希望能帮到你。 ╰( ̄▽ ̄)╭

【讨论】:

  • 我接受您的回答,因为 collectAndThen() 引导我实现了我想要实现的目标(通过对 OP 的编辑进一步澄清),尽管它与您的实际代码略有不同,谢谢
【解决方案2】:

如果您已经使用 Java 12,有一个新的收集器 Collectors.teeing() 允许使用两个独立的收集器进行收集,然后使用提供的 BiFunction 合并它们的结果。传递给结果收集器的每个元素都由两个下游收集器处理,然后使用指定的合并函数将它们的结果合并到最终结果中。因此,Collectors.teeing() 可能是一个不错的选择,因为您需要计数和求和。

Map<LocalDate, Result> pairedReportDateMRR = 
    revenueRecords.stream().collect(Collectors.groupingBy(RevenueRecorder::getReportDate,
                    Collectors.teeing(Collectors.counting(),
                            Collectors.summingInt(RevenueRecorder::getRevenue), Result::new)));
    System.out.println(pairedReportDateMRR); 

   //output: {2019-01-07={count=2, sum=46}, 2019-01-16={count=3, sum=224}}

出于测试目的,我使用了以下简单的静态类

static class Result {
    private Long count;
    private Integer sum;
    public Result(Long count, Integer sum) {
        this.count = count;
        this.sum = sum;
    }
    @Override
    public String toString() {
        return "{" + "count=" + count + ", sum=" + sum + '}';
    }
}

【讨论】:

  • @Holger 你是对的,你可以在一次收集中完成。我已经根据您的建议调整了我的回答。非常感谢。
  • 我会稍微减少缩进,以避免出现水平滚动条。
【解决方案3】:

首先,您无法生成地图Map&lt;LocalDate, Pair&lt;Integer, Integer&gt;&gt;,因为您想进行第二次分组,这意味着在同一日期您可能有多个客户代码,每个客户代码都有单独的计数器。

所以,如果我说得对,你不会得到类似 Map&lt;LocalDate, MutablePair&lt;Integer, Map&lt;String, Integer&gt;&gt;&gt; 的东西,如果它是正确的,试试这个代码 sn-p:

public static void main(String[] args) {
    String data = "C1,2019-1-7,12\n" +
            "C2,2019-1-7,34\n" +
            "C1,2019-1-16,56\n" +
            "C2,2019-1-16,78\n" +
            "C3,2019-1-16,90";

    Stream.of(data.split("\n")).forEach(System.out::println);

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d");
    List<RevenueRecorder> revenueRecords = Stream.of(data.split("\n")).map(line -> {
        String[] s = line.split(",");
        String clientCode = s[0];
        LocalDate reportDate = LocalDate.parse(s[1].trim(), formatter);
        int revenue = Integer.parseInt(s[2]);
        return new RevenueRecorder(clientCode, reportDate, revenue);
    }).collect(toList());

    Supplier<MutablePair<Integer, Map<String, Integer>>> supplier = () -> MutablePair.of(0, new HashMap<>());
    BiConsumer<MutablePair<Integer, Map<String, Integer>>, RevenueRecorder> accumulator = (pair, recorder) -> {
        pair.setLeft(pair.getLeft() + recorder.getRevenue());
        pair.getRight().merge(recorder.getClientCode(), 1, Integer::sum);
    };
    BinaryOperator<MutablePair<Integer, Map<String, Integer>>> combiner = (p1, p2) -> {
        p1.setLeft(p1.getLeft() + p2.getLeft());
        p2.getRight().forEach((key, val) -> p1.getRight().merge(key, val, Integer::sum));
        return p1;
    };

    Map<LocalDate, MutablePair<Integer, Map<String, Integer>>> pairedReportDateMRR = revenueRecords.stream()
            .collect(
                    groupingBy(RevenueRecorder::getReportDate,
                            Collector.of(supplier, accumulator, combiner))
            );

    System.out.println(pairedReportDateMRR);
}

【讨论】:

  • re “......在同一日期,您可能有多个客户代码......”看看上面@Hadi 的解决方案。除了 keyMapper 和 valueMapper 之外,他/她还使用 BinaryOperator 来解决键冲突。更多详情请见here,例如谢谢
  • 是的,但是他/她的解决方案会计算所有值,而不像您的示例中的这一部分那样按客户代码对它们进行分组:groupingBy(RevenueRecorder::getClientCode, Collectors.counting())
  • @shanlodh 如果 Hadi J 的代码解决了您的问题,那么整个任务描述都是错误的,因为客户端代码值没有相关性,而且您根本没有执行两个分组操作。还有一个例子是“一旦你明白了你真正想要什么,解决方案就很简单了”……
猜你喜欢
  • 2014-12-10
  • 2017-05-23
  • 1970-01-01
  • 2017-12-24
  • 1970-01-01
  • 1970-01-01
  • 2015-01-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多