Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does a java collecting stream run each getter twice?

I have a List with items, where each item is an object with a getter method I'm interested in. I want to run over the complete list to sum all those getter results.

When I do it with java 8 streams, it looks like this:

double currentProduction = itemList.stream().collect(
    Collectors.summingDouble((e) -> e.getProduction(param)));

In plain old java, it looks like this:

for (Item item : itemList) {
    currentProduction += item.getProduction(param);
}

Both methods yield exactly the same result, but my logger reports that for each item instance the getProduction() method is run TWICE in case of the java 8 stream solution. In the plain old java list iteration solution the getProduction method is just run once per instance, as expected.

As the getProduction method is quite costly, this is an issue for me.

Why is this? And what can I do about this (besides using just the for loop)?

like image 317
R Hoekstra Avatar asked Jul 21 '17 12:07

R Hoekstra


Video Answer


2 Answers

It is bug in implementation of Collectors.summingDouble.

You can use itemList.stream().mapToDouble(Class1::getProduction).sum() instead.

Details of bug:

Source of Collector implementation.

    return new CollectorImpl<>(
            () -> new double[3],
            (a, t) -> { sumWithCompensation(a, **mapper.applyAsDouble(t)**);
                        a[2] += **mapper.applyAsDouble(t)**;},
            (a, b) -> { sumWithCompensation(a, b[0]);
                        a[2] += b[2];
                        return sumWithCompensation(a, b[1]); },
            a -> computeFinalSum(a),
            CH_NOID);

Error is marked by **. They invoke mapper.applyAsDouble(t) twice.

like image 165
talex Avatar answered Sep 27 '22 20:09

talex


Here a small MCVE to demonstrate the issue addressed in bug report JDK-8151123 which is mentioned in the answer from talex

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class SO {
    static class CoffeeBabe {
        final Double value;
        CoffeeBabe(Double value) {
            this.value = value;
        }
        Double getProduction() {
            System.out.println("getProduction() for " + this.value);
            return value;
        }
    }

    public static void main(String[] args) {
        List<CoffeeBabe> itemList = Arrays.asList(
                new CoffeeBabe(13.0),
                new CoffeeBabe(14.0),
                new CoffeeBabe(15.0)
        );
        System.out.println("mapToDouble...");
        double sum = itemList.stream()
            .mapToDouble(CoffeeBabe::getProduction).sum();
        System.out.println("sum = " + sum);

        System.out.println("\nCollectors.summingDouble");
        sum = itemList.stream().collect(Collectors.summingDouble(
            CoffeeBabe::getProduction));
        System.out.println("sum = " + sum);
    }
}

output

mapToDouble...
getProduction() for 13.0
getProduction() for 14.0
getProduction() for 15.0
sum = 42.0

Collectors.summingDouble
getProduction() for 13.0
getProduction() for 13.0
getProduction() for 14.0
getProduction() for 14.0
getProduction() for 15.0
getProduction() for 15.0
sum = 42.0
like image 43
SubOptimal Avatar answered Sep 27 '22 18:09

SubOptimal