javalambdajava-streamjava-17

Math operations in stream foreach and updating the existing value object


BigDecimal rate = BigDecimal.ZERO;
BigDecimal fixedCost = BigDecimal.ZERO;
int SCALE = 16;
fees. stream.forEach(fee -> {
    switch (fee.FeeRateCode) {
        case POINT -> rate =rate.add(fee.rateValue().divide(BigDecimal.valueOf(10000), SCALE, RoundingMode.HALF_EVEN))
        case SHARE -> rate =rate.add(fee.rateValue().divide(priceValue, SCALE, RoundingMode.HALF_EVEN))
        case LOCAL -> fixedCost = fixedCost.add(fee.rateValue().divide(exchangeAmount, SCALE, RoundingMode.HALF_EVEN))
    }
}
CostModel.builder().fixedCost(fixedCost).rateValue(rate).build();

Getting error for declaring rate and fixedCost as final/effectively final. Is there any way I can fix this without declaring a local/class level variable, like in the streams foreach, I get model existing rate and fixedCost and update it based on the case?

Ex: (something like below)

CostModel costModel = CostModel.builder().build();
case POINT
    -> costModel.rateValue(rate
        -> rate.add(fee.rateValue().divide(
            BigDecimal.valueOf(10000), SCALE, RoundingMode.HALF_EVEN)))

Solution

  • According to the docs of the Java Tutorial

    a lambda expression can only access local variables and parameters of the enclosing block that are final or effectively final.

    This means that the expression supplied to the stream operation forEach() can only access variables or parameters declared as final or whose value is not changed within the expression (effectively final).

    In your case, if you really want to use streams to tackle this problem, you could define a custom Consumer<Fee> to handle the consumption and addition of each Fee.

    public class FeeConsumer implements Consumer<Fee> {
    
        private final int scale;
        private final BigDecimal priceValue;
        private final BigDecimal exchangeAmount;
    
        @Getter
        private BigDecimal rate = BigDecimal.ZERO;
        @Getter
        private BigDecimal fixedCost = BigDecimal.ZERO;
    
        public FeeConsumer(int scale, BigDecimal priceValue, BigDecimal exchangeAmount) {
            this.scale = scale;
            this.priceValue = priceValue;
            this.exchangeAmount = exchangeAmount;
        }
    
        @Override
        public void accept(Fee fee) {
            switch (fee.getFeeRateCode()) {
                case POINT:
                    rate = rate.add(fee.rateValue().divide(BigDecimal.valueOf(10000), scale, RoundingMode.HALF_EVEN));
                    break;
                case SHARE:
                    rate = rate.add(fee.rateValue().divide(priceValue, scale, RoundingMode.HALF_EVEN));
                    break;
                case LOCAL:
                    fixedCost = fixedCost.add(fee.rateValue().divide(exchangeAmount, scale, RoundingMode.HALF_EVEN));
            }
        }
    
        public void combine(FeeConsumer other) {
            this.rate = this.rate.add(other.rate);
            this.fixedCost = this.fixedCost.add(other.fixedCost);
        }
    }
    

    With this implementation, the stream within the method in charge of creating a CostModel could look like this:

    public CostModel createCostModel(List<Fee> fees, int scale, BigDecimal priceValue, BigDecimal exchangeAmount) {
        FeeConsumer feeConsumer = fees.stream()
            .collect(
                () -> new FeeConsumer(scale, priceValue, exchangeAmount),                           
                FeeConsumer::accept, 
                FeeConsumer::combine
             );
    
             return CostModel.builder()
                     .rateValue(feeConsumer.getRate())
                     .fixedCost(feeConsumer.getFixedCost())
                     .build();
        }
    

    Alternatively, if you cannot be bothered to create a whole new class to consume Fee objects, you could employ a Collector, specifically the Collectors.toMap(keyMapper, valueMapper, mergeFunction), to accumulate fees by their rate code. However, keep in mind that this solution is not as flexible nor as scalable as the first one.

    public CostModel createCostModel(List<Fee> fees, int scale, BigDecimal priceValue, BigDecimal exchangeAmount) {
        Map<FeeRateCode, BigDecimal> mapTempValues = fees.stream()
            .collect(Collectors.toMap(
                Fee::getFeeRateCode,
                fee -> {
                    if (fee.getFeeRateCode() == FeeRateCode.POINT) {
                        return fee.rateValue().divide(BigDecimal.valueOf(10000), scale, RoundingMode.HALF_EVEN);
                    }
                    if (fee.getFeeRateCode() == FeeRateCode.SHARE) {
                        return fee.rateValue().divide(priceValue, scale, RoundingMode.HALF_EVEN);    
                    }
                    return fee.rateValue().divide(exchangeAmount, scale, RoundingMode.HALF_EVEN);
                },
                BigDecimal::add)
            );
    
        // The rateValue is given by the sum of the values mapped by FeeRateCode.POINT and FeeRateCode.SHARE
        return CostModel.builder()
                    .rateValue(mapTempValues.getOrDefault(FeeRateCode.POINT, BigDecimal.ZERO).add(mapTempValues.getOrDefault(FeeRateCode.SHARE, BigDecimal.ZERO)))
                    .fixedCost(mapTempValues.getOrDefault(FeeRateCode.LOCAL, BigDecimal.ZERO))
                    .build();
        }
    

    For more info on how to collect or reduce values from streams, I recommend this article from the Java Tutorials.

    Demo

    Here is also a demo at OneCompiler with both solutions.