简体   繁体   English

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

[英]Java Stream: Grouping and counting by multiple fields

I have the following object: 我有以下对象:

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;
}

} }

I need to aggregate by year/month (yyyy-mm) and event type, and then count. 我需要按年/月(yyyy-mm)和事件类型进行汇总,然后计算。 For example the following list 例如以下列表

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")
);

should produce the following result: 应该产生以下结果:

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

Any idea if (and if so, how) I can achieve that with Streams API? 任何想法(如果是这样,如何)我可以用Streams API实现这一点?

If you do not want to define your own key, you could groupBy twice. 如果您不想定义自己的密钥,可以groupBy两次。 The result is the same, but in slightly different format: 结果是一样的,但格式略有不同:

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

And the result is: 结果是:

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

In case you don't want to create a new key class, as suggested by assylias, you can do a double groupingBy 如果你不想按照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()))
                   );

... followed by a nested print ...后面是嵌套打印

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

This prints 这打印

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

EDIT: I noticed the order of the dates was the opposite of the OP's expected solution. 编辑:我注意到日期的顺序与OP的预期解决方案相反。 Using the 3-parameter version of groupingBy you can specify a sorted map implementation 使用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()))
                   );

The same map.forEach(...) now prints 现在打印相同的map.forEach(...)

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

You could create a "key" class that contains the year/month and the event type: 您可以创建一个包含年/月和事件类型的“密钥”类:

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.
}

You can then use that key to group your events: 然后,您可以使用该键对事件进行分组:

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

which outputs: 哪个输出:

2017-04 EVENT1  1
2017-03 EVENT1  2
2017-04 EVENT2  2
2017-05 EVENT3  1
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)));

No need to use an Arrays.asList() method to get to a stream. 无需使用Arrays.asList()方法来获取流。 Use Stream.of() method directly to get a stream. 直接使用Stream.of()方法获取流。

Output : 输出

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

We can create method in POJO has that contains list of fields to use for grouping by, like below 我们可以在POJO中创建包含用于分组的字段列表的方法,如下所示

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

And the stream code, 和流代码,

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

Output would be: 输出将是:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM