Java Stream:按多个字段分组和计数

5

我有以下对象:

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实现这一功能吗?如果可以,应该如何实现呢?


你说你想按月份聚合,但结果也包含了年份。那么是按月份和年份聚合,还是按月份聚合但同时显示年份? - Eugene
我是指按年/月(yyyy-mm)聚合。我已经编辑了帖子 :) - Nick Melis
任何一个答案都可以符合要求 :) - Eugene
5个回答

8

如果您不想像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

编辑:我注意到时间顺序与发帖者期望的解决方案相反。使用groupingBy的三个参数版本,您可以指定排序的地图实现。
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

3
您可以创建一个包含年/月和事件类型的“key”类:
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

我喜欢这个解决方案,对我来说它似乎是最面向对象的。我会添加一个关于排序的注释,即在Group中实现compareTo方法,并使用TreeMap或带有自定义比较器的TreeMap - fps

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 来实现该目的。 - Malte Hartwig
1
@MalteHartwig 是的,我知道,但是原帖中说:我需要按月和事件类型聚合。这也可以很容易地改为年... - Eugene

0
我们可以在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}


0
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在他的示例中给出了一个List。你为什么不想使用它?除此之外,你的答案和我的一样。 - Robin Topper
为什么要使用Arrays类的静态方法创建列表,然后将该列表转换为流,而不是直接获取流?我没有看过你的答案。 - Avneet Paul

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