使用流如何创建摘要对象

Using streams how can I create a summary object

请假设我有以下数据结构

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

在另一种方法中,我将构建一个该类型的集合,如下所示:

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

我想流式传输这些以映射到总对象

一个看起来像这样的支付对象

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

我知道我可以使用 mapToDouble 一次在一列中执行此操作,但我想使用 reduce 或其他方法使它在一个流中发生。

您可以将自己的 Collector 实现到 Payment 对象:

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

没有理由在更短更易于阅读的地方使用 Streams,例如:

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

正如评论中所讨论的那样,这个解决方案不仅更短更清晰(IMO)而且更易于维护:例如,假设现在你有一个例外:你想继续对所有这些属性求和,但你想要排除第二个索引上的项目。与简单的 for 循环相比,将其添加到 reduce-verion 中有多容易?

同样有趣的是,此解决方案的内存占用量较小(因为 reduce 每次迭代都会创建一个额外的对象)并且在提供的示例中运行效率更高。

缺点:我唯一能找到的是万一我们正在处理的集合很大(数千或更多),在那种情况下我们应该使用带有 Stream.parallel 的 reduce 解决方案,但即便如此它也应该成为 done cautiously

通过以下方式与 JMH 进行基准测试:

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

结果:

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

您需要一个 BinaryOperator<Payment> accumulator 来组合两个 Payment

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

减少量将如下所示:

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

或(避免创建标识对象):

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

我不会为此使用流,但既然你问了:

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