Averaging across multiple fields with IntSummaryStatistics

▼魔方 西西 提交于 2020-11-28 02:15:24

问题


I'm trying to use Java 8 streams to create a single CarData object, which consists of an average of all the CarData fields in the list coming from getCars;

   CarData = new CarData();
   CarData.getBodyWeight returns Integer
   CarData.getShellWeight returns Integer

     List<CarData> carData = carResults.getCars();
    
    IntSummaryStatistics averageBodyWeight = carData.stream()
            .mapToInt((x) -> x.getBodyWeight())
            .summaryStatistics();
    
    averageBodyWeight.getAverage(); 


            IntSummaryStatistics averageShellWeight = carData.stream()
            .mapToInt((x) -> x.getShellWeight())
            .summaryStatistics();
    
    getShellWeight.getAverage(); 

I don't want to have to put each of these back together in my final returned result.

Visually, this is my list

getCars() : [
 {CarData: { getBodyWeight=10, getShellWeight=3 } }
 {CarData: { getBodyWeight=6, getShellWeight=5 } }
 {CarData: { getBodyWeight=8, getShellWeight=19 } }
]

and the output I'm trying to achieve is a single object that has the average of each of the fields I specify. not sure If I need to use Collectors.averagingInt or some combo of IntSummaryStatistics to achieve this. Easy to do across one field for either of these techniques, just not sure what I'm missing when using multiple integer fields.

 {CarData: { getBodyWeight=8, getShellWeight=9 } }

回答1:


You need to write your own Collector, something like this:

class CarDataAverage {
    public static Collector<CarData, CarDataAverage, Optional<CarData>> get() {
        return Collector.of(CarDataAverage::new, CarDataAverage::add,
                            CarDataAverage::combine,CarDataAverage::finish);
    }
    private long sumBodyWeight;
    private long sumShellWeight;
    private int count;
    private void add(CarData carData) {
        this.sumBodyWeight += carData.getBodyWeight();
        this.sumShellWeight += carData.getShellWeight();
        this.count++;
    }
    private CarDataAverage combine(CarDataAverage that) {
        this.sumBodyWeight += that.sumBodyWeight;
        this.sumShellWeight += that.sumShellWeight;
        this.count += that.count;
        return this;
    }
    private Optional<CarData> finish() {
        if (this.count == 0)
            return Optional.empty();
        // adjust as needed if averages should be rounded
        return Optional.of(new CarData((int) (this.sumBodyWeight / this.count),
                                       (int) (this.sumShellWeight / this.count)));
    }
}

You then use it like this:

List<CarData> list = ...

Optional<CarData> averageCarData = list.stream().collect(CarDataAverage.get());



回答2:


Starting with JDK 12, you can use the following solution:

CarData average = carData.stream().collect(Collectors.teeing(
    Collectors.averagingInt(CarData::getBodyWeight),
    Collectors.averagingInt(CarData::getShellWeight),
    (avgBody, avgShell) -> new CarData(avgBody.intValue(), avgShell.intValue())));

For older Java versions, you can do either, add the teeing implementation of this answer to your code base and use it exactly as above or create a custom collector tailored to your task, as shown in Andreas’ answer.

Or consider that streaming twice over a List in memory is not necessarily worse than doing two operations in one stream, both, readability- and performance-wise.

Note that calling intValue() on Double objects has the same behavior as the (int) casts in Andreas’ answer. So in either case, you have to adjust the code if other rounding behavior is intended.

Or you consider using a different result object, capable of holding two floating point values for the averages.



来源:https://stackoverflow.com/questions/63809505/averaging-across-multiple-fields-with-intsummarystatistics

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!