Reputation: 4209
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
i.e.
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.
Upvotes: 1
Views: 159
Reputation: 53525
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
Upvotes: 1
Reputation: 49606
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);
Upvotes: 1
Reputation: 11621
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()));
Upvotes: 2
Reputation: 311188
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;
}));
Upvotes: 3