简体   繁体   中英

Using Java Streams to group together a List of objects by an attribute and reduce them to a new list of object with the average of another attribute

I have a list of SensorSample POJOs

public class SensorSample {

    private Device.SensorType sensorType; // This is an enum
    private double sample;
    private long timestamp;

    // Constructor

    // Setters

    // Getters

}

I need to group them by the timestamp , so that all the SensorSample s of the same day are together. Then I need to reduce them so that I have only one SensorSample for each day and the value of its sample is the average of the value of the sample of all the objects for that day. Is there a way to do that with Streams?

So far I got this to group them together:

Map<Long, List<SensorSample>> yearSamples = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY)));

But I don't know how to go any further.

Something like this, I think. To find the average number of a group:

Map<Long, Double> averages = samples.stream()
  .collect(groupingBy(SensorSample::getTimestamp,
   averagingDouble(SensorSample::getSample)));

I didn't expand out your formula for the day, I think it's more readable if I just called getTimestamp and leave the details out. Your code might also be more readable if you added a getDay method to SensorSample .

Also this would be easier to test if you provided an MCVE, as it's a little hard to test the code above with only one partial class to go on.

it seems as if you want a List<SensorSample> as a result where each group after the groupingBy is reduced into a single SensorSample .

List<SensorSample> result = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY))
                .entrySet()
                .stream()
                .map(e -> {
                    SensorSample sensorSample = new SensorSample();
                    sensorSample.setTimestamp(e.getKey());
                    double average = e.getValue().stream()
                            .mapToDouble(SensorSample::getSample)
                            .average().orElse(0);
                    sensorSample.setSample(average);
                    sensorSample.setSensorType(e.getValue().get(0).getSensorType());
                    return sensorSample;
                }).collect(Collectors.toList());

The map logic seems a little bit large thus I'd consider refactoring it out to a method as such:

private static SensorSample apply(Map.Entry<Long, List<SensorSample>> e) {
        SensorSample sensorSample = new SensorSample();
        sensorSample.setTimestamp(e.getKey());
        double average = e.getValue().stream()
                .mapToDouble(SensorSample::getSample)
                .average().orElse(0);
        sensorSample.setSample(average);
        sensorSample.setSensorType(e.getValue().get(0).getSensorType());
        return sensorSample;
}

Then the stream pipeline would become:

List<SensorSample> result = samples.stream()
                .collect(groupingBy(sample -> SECONDS_IN_A_DAY*Math.floorDiv(sample.getTimestamp(), SECONDS_IN_A_DAY))
                .entrySet()
                .stream()
                .map(Main::apply)
                .collect(Collectors.toList());

Where Main is the class containing the apply method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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