9

I am curious, how to sum up multiple variables in a java8 stream.

Integer wCPU = 0;
Double wnetwork = 0.0;
Double wMem = 0.0;

this.slaContractList.forEach(sla -> {
    wCPU += sla.getNumberOfCPUs();
    wnetwork += sla.getNetworkBandwith();
    wMem += sla.getMemory();
});

However, this does not compile as the variable in the lambda expression should be final.

Sahil Aggarwal
  • 1,311
  • 1
  • 12
  • 29
Georg Heiler
  • 16,916
  • 36
  • 162
  • 292
  • 1
    You want a custom collector, look at http://stackoverflow.com/questions/37187541/java-8-stream-add-elements-to-list-and-sum/37188002#37188002 – Tunaki Nov 12 '16 at 23:24
  • What are the downsides of the hack? it seems to have fewer lines of code which is nice. – Georg Heiler Nov 12 '16 at 23:29
  • 1
    You said it... it's a hack. Run it in parallel, it will likely not produce the right output because `+=`, like `++`, [is not an atomic operation](http://stackoverflow.com/questions/25168062/why-is-i-not-atomic). – Tunaki Nov 12 '16 at 23:33
  • What do you think about the latest answer added by @Lukasz that looks pretty neat as well and does not have the downsides of the Hack. – Georg Heiler Nov 12 '16 at 23:46
  • Solved with mutablePair from commons collections – Gaurav Nov 12 '22 at 06:51

4 Answers4

10

Assuming slaContractList is a list of SlaContract objects, and it has constructor SlaContract(numberOfCPUs, networkBandwith, memory) you can:

SlaContract sumContract = slaContractList.stream()
    .reduce(new SlaContract(0, 0.0, 0.0), (sla1, sla2) -> {
         return new SlaContract(sla1.getNumberOfCPUs() + sla2.getNumberOfCPUs(), sla1.getworkBandwith() + sla2.getworkBandwith(), sla1.getMemory() + sla2.getMemory());
    });

Double wnetwork = sumContract.getworkBandwith();
Double wMem = sumContract.getMemory();
Integer wCPU = sumContract.getNumberOfCPUs();

The same solution, but for simple class:

Point sumPoint = pointsList.stream()
    .reduce(new Point(0, 0), (p1, p2) -> {
         return new Point(p1.x + p2.x, p1.y + p2.y);
    });
Łukasz
  • 2,131
  • 1
  • 13
  • 28
  • 4
    `reduce` should never modify its input parameter during accumulation. This will break when run in parallel, it should always return a new object containing the accumulation. [See also](http://stackoverflow.com/questions/23869930/is-the-accumulator-of-reduce-in-java-8-allowed-to-modify-its-arguments). You want to use `collect` here. – Tunaki Nov 13 '16 at 00:10
7

Try to use Stream.reduce and Stream.sum:

Double wnetwork = slaContractList.stream()
            .mapToDouble(sla -> sla.getNetworkBandwith())
            .sum();

Double wMem = slaContractList.stream()
            .mapToDouble(sla -> sla.getMemory())
            .sum();

Integer wCPU = slaContractList.stream()
            .mapToInt(sla -> sla.getNumberOfCPUs())
            .sum();

See https://docs.oracle.com/javase/tutorial/collections/streams/reduction.html

The advantage of using stream is option of use parallelStream() instead of stream(). In some situations it can be more efficient than simple loop.

Łukasz
  • 2,131
  • 1
  • 13
  • 28
  • so you would use a map for each attribute? Isn't this slower as a single loop? – Georg Heiler Nov 12 '16 at 21:56
  • The thing is it is basically a stream of tuples. And I believe it would be quicker if each value in the tuple would be summed up separately, but within a single pass of the loop. – Georg Heiler Nov 12 '16 at 21:57
  • Well, that's the way you can sum the stream. Compare this solution with others not using streams. Check `parallelStream()` instead of `stream()` as well. – Łukasz Nov 12 '16 at 22:04
  • `long t0 = System.nanoTime();` //solution here `long t1 = System.nanoTime();` `long millis = TimeUnit.NANOSECONDS.toMillis(t1 - t0);` `System.out.println(String.format("solution took: %d ms", millis));` – Łukasz Nov 12 '16 at 22:06
  • Just out of curiosity, could you please which solution is faster in your situation? (just copy some code from previous comment) – Łukasz Nov 12 '16 at 22:25
  • I am not sure if this would be useful e.g. we are using this code in a round based simulation where the rounds are executed sequentially with a certain delay. – Georg Heiler Nov 12 '16 at 22:43
  • OK. In first comment you asked if it isn't slower than single loop, so I thought it matters. – Łukasz Nov 12 '16 at 22:48
  • It does not matter right now. I wanted to learn how to approach such a problem. But it might in the future. Thank you very much for the answer. – Georg Heiler Nov 12 '16 at 23:20
4

Just to do a sum, i would use the sum operation of streams just like in Łukasz answer, but, for a more general solution for resolving the "final problem" you can use the classes of java.util.concurrent.atomic. It is intented to be used in stream and is thread-safe, so could be used in a parallel stream.

AtomicInteger wCPU = new AtomicInteger();
DoubleAccumulator wnetwork = new DoubleAccumulator(Double::sum,0.d);
DoubleAccumulator wMem = new DoubleAccumulator(Double::sum,0.d);

this.slaContractList.forEach(sla -> {
    wCPU.addAndGet(sla.getNumberOfCPUs());
    wnetwork.accumulate(sla.getNetworkBandwith());
    wMem.accumulate(sla.getMemory());
});

Now you see that there are 2 kind of implementation: the Accumulator and the Atomic, the choice between these 2 is another question:

java 8 : Are LongAdder and LongAccumulator preferred to AtomicLong?

Community
  • 1
  • 1
pdem
  • 3,880
  • 1
  • 24
  • 38
3

I would do a simple hack like this:

    Integer[] wCPU = new Integer[1];
    Double[] wnetwork = new Double[1];
    Double[] wMem = new Double[1];

    this.slaContractList.forEach(sla -> {
        wCPU[0] += sla.getNumberOfCPUs();
        wnetwork[0] += sla.getNetworkBandwith();
        wMem[0] += sla.getMemory();
    });

It's optional to have the final keyword for both , as in Java 8 they have introduced the effectively final concept. It means, you have assigned only once.

Tunaki
  • 132,869
  • 46
  • 340
  • 423
Jude Niroshan
  • 4,280
  • 8
  • 40
  • 62