Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding multiple fields in Java streams (and conditional stream operations)

Let's say I have this class:

public class Thing {
    private BigDecimal field1;
    private BigDecimal field2;

    private BigDecimal otherField1;
    private BigDecimal otherField2;
    private BigDecimal otherField3;
}

and in another class I for-each over a List<Thing> things, adding field1 and field2 to a sum that I return when the iteration is done.

But what I want is to accomplish that with Java streams. The following is what I have--it works, but I feel like there has to be a way to condense it down to one stream:

public BigDecimal addFields(List<Thing> things) {
    BigDecimal field1sum = things.parallelStream()
                                     .filter(thing -> thing.getField1() != null)
                                     .map(Thing::getField1)
                                     .reduce(BigDecimal.ZERO, BigDecimal::add);

     BigDecimal field2sum = things.parallelStream()
                                     .filter(thing -> thing.getField2() != null)
                                     .map(Thing::getField2)
                                     .reduce(BigDecimal.ZERO, BigDecimal::add);
     return field1sum.add(field2sum);
}

I suspect the answer is the reduce() method that takes three arguments, one of which is a BiFunction, but I haven't been able to figure out how to make that work. Edit: I think I could pass in (x,y) -> x.add(y) to reduce(), but then the question becomes how do I map() both of those fields?

Additionally, is it possible/how would I go about turning this imperative code into a functional stream?

public BigDecimal addOtherFields(List<Thing> things) {
    BigDecimal result = BigDecimal.ZERO;
    for (Thing thing : things) {
        if (thing.getOtherField2() != null) {
            BigDecimal otherField2 = thing.getOtherField2();
            otherField2 = thing.getOtherField1().subtract(otherField2);
            result = result.add(otherField2);
         } else if (thing.getOtherField3() != null) {
            BigDecimal otherField3 = thing.getOtherField3();
            otherField3 = thing.getOtherField1.subtract(otherField3);
            result = result.add(otherField3);
         }
     }
     return result;
 }

Or, to be a bit more precise, how would I handle that conditional check in a stream based approach? I was trying to filter() things out without success.

like image 774
dxh3707 Avatar asked Apr 07 '16 19:04

dxh3707


Video Answer


2 Answers

Use collect(), with a custom collector helper, not unlike IntSummaryStatistics.

things.stream()
      .collect(ThingCollectorHelper::new, 
               ThingCollectorHelper::accept,
               ThingCollectorHelper::combine);

Your helper class will be something like:

class ThingCollectorHelper {
    BigDecimal sum1 = BigDecimal.ZERO;
    BigDecimal sum2 = BigDecimal.ZERO;

    void accept(Thing t) {
        if (t.field1 != null)
            sum1 = sum1.plus(t.field1);
        if (t.field2 != null)
            sum2 = sum2.plus(t.field2);
    }

    void combine(ThingCollectorHelper other) {
        sum1 = sum1.plus(other.sum1);
        sum2 = sum2.plus(other.sum2);
    }

}

like image 178
Brian Goetz Avatar answered Oct 16 '22 14:10

Brian Goetz


Since you're going to treat the fields uniformly, you may consider flatMap:

public BigDecimal addFields(List<Thing> things) {
    return things.parallelStream()
        .flatMap(thing -> Stream.of(thing.getField1(), thing.getField2()))
        .filter(Objects::nonNull)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
}
public BigDecimal addOtherFields(List<Thing> things) {
    return things.parallelStream()
        .flatMap(thing ->
            Stream.of(thing.getOtherField2(), thing.getOtherField3())
                .filter(Objects::nonNull)
                .map(thing.getOtherField1()::subtract)
        ).reduce(BigDecimal.ZERO, BigDecimal::add);
}
like image 3
Holger Avatar answered Oct 16 '22 15:10

Holger