简体   繁体   中英

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.

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);
    }

}

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);
}

In your first method, you want to sum all field1 and field2 together, ignoring null . You can do this with a single Stream pipeline by, as you hinted, the 3 argument reduce method.

In this case, the identity is still BigDecimal.ZERO . The accumulator function adds to the current accumulated result each field if they're not null. Finally, the combiner, which is only used for parallel processing, adds the two BigDecimal values.

public BigDecimal addFields(List<Thing> things) {
    return things.parallelStream().reduce(BigDecimal.ZERO, (a, t) -> {
         if (t.getField1() != null) a = a.add(t.getField1());
         if (t.getField2() != null) a = a.add(t.getField2());
         return a;
     }, BigDecimal::add);
}

The same goes for your second example where, in this case, you want to sum the difference between otherField1 and otherField2 or otherField3 depending on whether or not they are null :

public BigDecimal addOtherFields(List<Thing> things) {
    return things.stream().reduce(BigDecimal.ZERO, (a, t) -> {
        if (t.getOtherField2() != null) {
            return a.add(t.getOtherField1().subtract(t.getOtherField2()));
        } else if (t.getOtherField3() != null) {
            return a.add(t.getOtherField1().subtract(t.getOtherField3()));
        }
        return a;
    }, BigDecimal::add);
 }

Another equivalent way of solving this task would be to use map() : in that case, you would map each element into the value you want to sum, and reduce the Stream by summing all BigInteger s. For example, in the first example, you would map each Thing into the sum of field1 and field2 , taking into account their potential null -ness.

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