简体   繁体   中英

Using streams how can I create a summary object

Please assume I have the following data structure

public class Payment {
    String paymentType;
    double price;
    double tax;
    double total;

    public Payment(String paymentType, double price, double tax, double total) {
        super();
        this.paymentType = paymentType;
        this.price = price;
        this.tax = tax;
        this.total = total;
    }
    public String getPaymentType() {
        return paymentType;
    }
    public void setPaymentType(String paymentType) {
        this.paymentType = paymentType;
    }
    public double getPrice() {
        return price;
    }
    public void setPrice(double price) {
        this.price = price;
    }
    public double getTax() {
        return tax;
    }
    public void setTax(double tax) {
        this.tax = tax;
    }
    public double getTotal() {
        return total;
    }
    public void setTotal(double total) {
        this.total = total;
    }
}

In another method I would have a collection built of that type as follows:

private Payment generateTotal() {
    Collection<Payment> allPayments = new ArrayList<>();
    allPayments.add(new Payment("Type1", 100.01, 1.12, 101.13));
    allPayments.add(new Payment("Type2", 200.01, 2.12, 202.13));
    allPayments.add(new Payment("Type3", 300.01, 3.12, 303.13));
    allPayments.add(new Payment("Type4", 400.01, 4.12, 404.13));
    allPayments.add(new Payment("Type5", 500.01, 5.12, 505.13));

    //Generate the total with a stream and return

    return null;
}

I would like to stream these to map to a total object

ie

A payment object that looks like this

paymentType = "Total";
price = sum(payment.price);
tax = sum(payment.tax);
total = sum(payment.total);

I know I can do this with mapToDouble one column at a time, but I would like to use a reduce or something to have this happen in one stream.

You could implement your own Collector to a Payment object:

Payment total =
    allPayments.stream()
               .collect(Collector. of(
                   () -> new Payment("Total", 0.0, 0.0, 0.0),
                   (Payment p1, Payment p2) -> {
                       p1.setPrice(p1.getPrice() + p2.getPrice());
                       p1.setTax(p1.getTax() + p2.getTax());
                       p1.setTotal(p1.getTotal() + p2.getTotal());
                   },
                   (Payment p1, Payment p2) -> {
                       p1.setPrice(p1.getPrice() + p2.getPrice());
                       p1.setTax(p1.getTax() + p2.getTax());
                       p1.setTotal(p1.getTotal() + p2.getTotal());
                       return p1;
                   }));

There is no reason to use Streams where it's shorter and easier to read something like:

    Payment sum = new Payment("Total", 0, 0, 0);
    allPayments.forEach(p -> {
        sum.price += p.price;
        sum.tax += p.tax;
        sum.total += p.total;
    });

As discussed in the comments, this solution is not only shorter and cleaner (IMO) but also easier to maintain: for example, say that now you have an exception: you want to continue summing all these attributes but you want to exclude the item on the second index. How easy will it be to add it to a reduce-verion vs a simple for-loop?

What's also interesting is that this solution has a smaller memory footprint (because reduce creates an additional object every iteration) and runs more efficiently with the provided example.

Drawback: the only one I could find is in case the collection we're processing is huge (thousands or more), in that case we should use the reduce solution with Stream.parallel but even then it should be done cautiously

Benchmarked with JMH in the following way:

@Benchmark
public Payment loopIt() {
    Collection<Payment> allPayments = new ArrayList<>();
    allPayments.add(new Payment("Type1", 100.01, 1.12, 101.13));
    allPayments.add(new Payment("Type2", 200.01, 2.12, 202.13));
    allPayments.add(new Payment("Type3", 300.01, 3.12, 303.13));
    allPayments.add(new Payment("Type4", 400.01, 4.12, 404.13));
    allPayments.add(new Payment("Type5", 500.01, 5.12, 505.13));
    Payment accum = new Payment("Total", 0, 0, 0);

    allPayments.forEach(x -> {
        accum.price += x.price;
        accum.tax += x.tax;
        accum.total += x.total;
    });
    return accum;
}

@Benchmark
public Payment reduceIt() {
    Collection<Payment> allPayments = new ArrayList<>();
    allPayments.add(new Payment("Type1", 100.01, 1.12, 101.13));
    allPayments.add(new Payment("Type2", 200.01, 2.12, 202.13));
    allPayments.add(new Payment("Type3", 300.01, 3.12, 303.13));
    allPayments.add(new Payment("Type4", 400.01, 4.12, 404.13));
    allPayments.add(new Payment("Type5", 500.01, 5.12, 505.13));
    return
        allPayments.stream()
            .reduce(
                new Payment("Total", 0, 0, 0),
                (sum, each) -> new Payment(
                    sum.getPaymentType(),
                    sum.getPrice() + each.getPrice(),
                    sum.getTax() + each.getTax(),
                    sum.getTotal() + each.getTotal()));
}

Results:

Result "play.Play.loopIt":
  49.838 ±(99.9%) 1.601 ns/op [Average]
  (min, avg, max) = (43.581, 49.838, 117.699), stdev = 6.780
  CI (99.9%): [48.236, 51.439] (assumes normal distribution)


# Run complete. Total time: 00:07:36

Benchmark    Mode  Cnt   Score   Error  Units
Play.loopIt  avgt  200  49.838 ± 1.601  ns/op

Result "play.Play.reduceIt":
  129.960 ±(99.9%) 4.163 ns/op [Average]
  (min, avg, max) = (109.616, 129.960, 212.410), stdev = 17.626
  CI (99.9%): [125.797, 134.123] (assumes normal distribution)


# Run complete. Total time: 00:07:36

Benchmark      Mode  Cnt    Score   Error  Units
Play.reduceIt  avgt  200  129.960 ± 4.163  ns/op

I wouldn't use streams for this, but since you asked:

    Payment total =
            allPayments.stream()
                    .reduce(
                            new Payment("Total", 0, 0, 0),
                            (sum, each) -> new Payment(
                                    sum.getPaymentType(),
                                    sum.getPrice() + each.getPrice(),
                                    sum.getTax() + each.getTax(),
                                    sum.getTotal() + each.getTotal()));

You need a BinaryOperator<Payment> accumulator for combining two Payment s:

public static Payment reduce(Payment p1, Payment p2) {
    return new Payment("Total", 
            p1.getPrice() + p2.getPrice(), 
            p1.getTax() + p2.getTax(), 
            p1.getTotal() + p2.getTotal()
    );
}

and the reduction will look like:

Payment payment = allPayments.stream().reduce(new Payment(), Payment::reduce);

or (to avoid identity object creation):

Optional<Payment> oPayment = allPayments.stream().reduce(Payment::reduce);

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