【问题标题】:Java Stream: Grouping and counting by multiple fieldsJava Stream:按多个字段分组和计数
【发布时间】:2017-10-13 19:43:11
【问题描述】:

我有以下对象:

class Event {
private LocalDateTime when;
private String what;

public Event(LocalDateTime when, String what) {
  super();
  this.when = when;
  this.what = what;
}

public LocalDateTime getWhen() {
  return when;
}

public void setWhen(LocalDateTime when) {
  this.when = when;
}

public String getWhat() {
  return what;
}

public void setWhat(String what) {
  this.what = what;
}

}

我需要按年/月 (yyyy-mm) 和事件类型汇总,然后计数。例如下面的列表

List<Event> events = Arrays.asList(
  new Event(LocalDateTime.parse("2017-03-03T09:01:16.111"), "EVENT1"),
  new Event(LocalDateTime.parse("2017-03-03T09:02:11.222"), "EVENT1"),
  new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT1"), 
  new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT2"),
  new Event(LocalDateTime.parse("2017-04-03T09:06:16.444"), "EVENT2"),
  new Event(LocalDateTime.parse("2017-05-03T09:01:26.555"), "EVENT3")
);

应该产生以下结果:

Year/Month  Type  Count
2017-03     EVENT1    2  
2017-04     EVENT1    1
2017-04     EVENT2    2
2017-04     EVENT3    1

是否知道(如果是,如何)我可以使用 Streams API 实现这一目标?

【问题讨论】:

  • 您说要按月汇总,但结果中还包含年份。 年的月份也是如此;还是按月汇总但同时显示年份?
  • 我的意思是按年/月 (yyyy-mm) 汇总。我已经编辑了帖子:)
  • 那么这里的任何答案都符合条件:)

标签: java java-8 java-stream


【解决方案1】:

如果您不想按照 assylias 的建议创建新的密钥类,您可以使用双重 groupingBy

Map<YearMonth,Map<String,Long>> map = 
     events.stream()
           .collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()),
                    Collectors.groupingBy(x -> x.getWhat(), Collectors.counting()))
                   );

... 后跟一个嵌套打印

map.forEach((k,v)-> v.forEach((a,b)-> System.out.println(k + " " +  a + " " + b)));

打印出来

2017-05 EVENT3 1
2017-04 EVENT2 2
2017-04 EVENT1 1
2017-03 EVENT1 2

编辑:我注意到日期的顺序与 OP 的预期解决方案相反。使用 groupingBy 的 3 参数版本,您可以指定排序映射实现

Map<YearMonth,Map<String,Long>> map = 
     events.stream()
           .collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()), TreeMap::new, 
                    Collectors.groupingBy(x -> x.getWhat(), Collectors.counting()))
                   );

现在打印相同的map.forEach(...)

2017-03 EVENT1 2
2017-04 EVENT2 2
2017-04 EVENT1 1
2017-05 EVENT3 1

【讨论】:

    【解决方案2】:

    您可以创建一个包含年/月和事件类型的“键”类:

    class Group {
      private YearMonth ym;
      private String type;
    
      public Group(Event e) {
        this.ym = YearMonth.from(e.getWhen());
        this.type = e.getWhat();
      }
    
      //equals, hashCode, toString etc.
    }
    

    然后您可以使用该键对您的事件进行分组:

    Map<Group, Long> result = events.stream()
                    .collect(Collectors.groupingBy(Group::new, Collectors.counting()));
    result.forEach((k, v) -> System.out.println(k + "\t" + v));
    

    哪个输出:

    2017-04 EVENT1  1
    2017-03 EVENT1  2
    2017-04 EVENT2  2
    2017-05 EVENT3  1
    

    【讨论】:

    • 我喜欢这个解决方案,它对我来说似乎是最OO的。我会添加关于排序的评论,即在Group 中实现compareTo 并使用TreeMap 或带有自定义比较器的TreeMap
    【解决方案3】:

    如果您不想定义自己的密钥,您可以groupBy 两次。结果相同,但格式略有不同:

     System.out.println(events.stream()
                .collect(Collectors.groupingBy(e -> YearMonth.from(e.getWhen()),
                        Collectors.groupingBy(Event::getWhat, Collectors.counting()))));
    

    结果是:

     {2017-05={EVENT3=1}, 2017-04={EVENT2=2, EVENT1=1}, 2017-03={EVENT1=2}}
    

    【讨论】:

    • getMonth 分组不会考虑年份,其他答案使用 YearMonth。
    • @MalteHartwig 是的,我知道,但 OP 说:我需要按月份和事件类型汇总。它也可以很容易地更改为年份...
    【解决方案4】:
    final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM");
        Stream.of(
                new Event(LocalDateTime.parse("2017-03-03T09:01:16.111"), "EVENT1"),
                new Event(LocalDateTime.parse("2017-03-03T09:02:11.222"), "EVENT1"),
                new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT1"),
                new Event(LocalDateTime.parse("2017-04-03T09:04:11.333"), "EVENT2"),
                new Event(LocalDateTime.parse("2017-04-03T09:06:16.444"), "EVENT2"),
                new Event(LocalDateTime.parse("2017-05-03T09:01:26.555"), "EVENT3")
                ).collect(Collectors.groupingBy(event -> 
                   dateTimeFormatter.format(event.getWhen()),
                   Collectors.groupingBy(Event::getWhat, counting())))
                 .forEach((whenDate,v) -> v.forEach((whatKey,counter) -> 
                    System.out.println(whenDate+ " "+ whatKey+" "+counter)));
    

    无需使用 Arrays.asList() 方法来获取流。直接使用 Stream.of() 方法获取流。

    输出

    2017-03 EVENT1 2
    2017-04 EVENT2 2
    2017-04 EVENT1 1
    2017-05 EVENT3 1
    

    【讨论】:

    • OP 在他的例子中给出了一个列表。你为什么不想使用它?除此之外,你的答案和我的一样。
    • 既然可以直接获取流,为什么还要使用 Arrays 类的静态方法创建列表,然后将该列表转换为流?我没看你的回答。
    【解决方案5】:

    我们可以在 POJO 中创建包含用于分组的字段列表的方法,如下所示

    public String getWhenAndWhat() {
        return YearMonth.from(when) + ":" + what; //you can use delimiters like ':','-',','
    }
    

    还有流代码,

    System.out.println(events.stream()
                .collect(Collectors.groupingBy(Event::getWhenAndWhat, Collectors.counting())));
    

    输出将是:

    {2017-05:EVENT3=1, 2017-04:EVENT1=1, 2017-04:EVENT2=2, 2017-03:EVENT1=2}

    【讨论】:

      猜你喜欢
      • 2018-07-24
      • 1970-01-01
      • 2014-11-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多