繁体   English   中英

使用Streams API收集器平均BigDecimals

[英]Averaging BigDecimals using Streams API Collectors

当前基于双重奖品的方法。

public Map<String, BigDecimal> averageProductPriceInCategory() {

    return shopping.entrySet()
            .stream()
            .flatMap(e -> e.getValue().keySet().stream())
            .collect(Collectors.groupingBy(Product::getCategory,
                    Collectors.averagingDouble(Product::getPrize)));
}

购物基本上是一幅地图: Map<Client, Map<Product,Integer>>

  • 外键代表客户
  • 内部键代表产品。 产品类成员是名称,类别,价格 (以前是double类型)-要使用price作为BigDecimal类型将提供的代码重构为一个代码
  • 内部地图值(整数)代表属于特定客户的指定产品的数量

以下代码段仅可用于计算属于指定类别的产品的总奖金。 不确定如何使用BigDecimals计算有关类别的平均产品奖

Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.entrySet().stream()
                .flatMap(e -> e.getValue().keySet().stream())
                .collect(Collectors.groupingBy(Product::getCategory,
                        Collectors.mapping(Product::getPrize,
                                Collectors.reducing(BigDecimal.ZERO, BigDecimal::add))));

看一下Collectors.averagingDoubleCollectors.averagingInt的实现方式。

public static <T> Collector<T, ?, Double>
averagingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<>(
            () -> new long[2],
            (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; },
            (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
            a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID);
}

本质上,您需要一个可变的累积类型,该类型应具有一个BigDecimal它是产品价格的总和)和一个int它是要处理的许多产品)。 有了这个,问题归结为编写一个简单的Collector<Product, AccumulationType, BigDecimal>

我简化了一个示例,并删除了getters / setters和all-args构造函数。 除了使用嵌套类ProductPriceSummary ,您还可以将任何可变的holder类用于2个元素。

class AverageProductPriceCollector implements Collector<Product, AverageProductPriceCollector.ProductPriceSummary, BigDecimal> {

    static class ProductPriceSummary {

        private BigDecimal sum = BigDecimal.ZERO;
        private int n;

    }

    @Override
    public Supplier<ProductPriceSummary> supplier() {
        return ProductPriceSummary::new;
    }

    @Override
    public BiConsumer<ProductPriceSummary, Product> accumulator() {
        return (a, p) -> {
            // if getPrize() still returns double
            // a.sum = a.sum.add(BigDecimal.valueOf(p.getPrize()));

            a.sum = a.sum.add(p.getPrize());
            a.n += 1;
        };
    }

    @Override
    public BinaryOperator<ProductPriceSummary> combiner() {
        return (a, b) -> {
            ProductPriceSummary s = new ProductPriceSummary();
            s.sum = a.sum.add(b.sum);
            s.n = a.n + b.n;

            return s;
        };
    }

    @Override
    public Function<ProductPriceSummary, BigDecimal> finisher() {
        return s -> s.n == 0 ?
                   BigDecimal.ZERO :
                   s.sum.divide(BigDecimal.valueOf(s.n), RoundingMode.CEILING);
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.emptySet();
    }

}

为了理解目的,我将操作分为两个步骤。 如果您愿意,可以将两个步骤结合在一起。

    Map<String, BigDecimal[]> stringMap = shopping.entrySet()
            .stream()
            .flatMap(e -> e.getValue().keySet().stream())
            .collect(Collectors.groupingBy(Product::getCategory,Collectors.collectingAndThen(Collectors.toList(),l -> l.stream().map(Product::getPrize)
                    .map(bd -> new BigDecimal[]{bd, BigDecimal.ONE})
                    .reduce((a, b) -> new BigDecimal[]{a[0].add(b[0]), a[1].add(BigDecimal.ONE)})
                    .get()
            )));

    Map<String, BigDecimal> stringBigDecimalMap = stringMap.entrySet().stream()
            .collect(Collectors.toMap(Map.Entry::getKey,e -> e.getValue()[0].divide(e.getValue()[1])));

说明:

  • 在第一个操作中,分组后,BigDecimals的流被映射为BigDecimal的两个元素数组的流,其中第一个元素是原始流中的元素,第二个元素是值为1的占位符。
  • 在减少a(a,b)的值在所述第一元件和第二元件的部分计数的部分和。 b元素的第一个元素包含要添加到总和的每个BigDecimal值。 不使用b的第二个元素。
  • 如果列表为空或仅包含空值,则reduce返回一个可选的内容,该内容将为空。
    • 如果Optional不为空,则Optional.get()函数将返回BigDecimal的两个元素数组,其中BigDecimals的总和在第一个元素中,而BigDecimals的计数在第二个元素中。
    • 如果Optional为空,则将引发NoSuchElementException。
  • 通过将总和除以计数来计算平均值。 这是针对中间映射Map<String, BigDecimal[]> stringMap每个条目完成的

这基于[Double|Int]Pipeline.average()的源代码。 它使用一个数组存储项数(在索引0 )和总数(在索引1 )。

public Map<String, BigDecimal> averageProductPriceInCategory() {
  return shopping.entrySet().stream()
      .flatMap(entry -> entry.getValue().keySet().stream())
      .collect(Collectors.groupingBy(
          Product::getCategory,
          Collector.of(
              () -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
              (array, product) -> {
                array[0] = array[0].add(BigDecimal.ONE);
                array[1] = array[1].add(product.getPrice());
              },
              (left, right) -> {
                left[0] = left[0].add(right[0]);
                left[1] = left[1].add(right[1]);
                return left;
              },
              array -> array[0].compareTo(BigDecimal.ONE) <= 0 
                       ? array[1] 
                       : array[1].divide(array[0], RoundingMode.HALF_UP)
          )
      ));
}

这有一些缺点:

  1. 不方便在多个地方使用。
  2. 不一定容易遵循。
  3. 将计数存储为BigDecimal ,在此情况下,使用intlong会更有意义。

这些问题可以通过将收集器提取到自定义类中来解决(就像Andrew的回答一样)。

您可以这样创建自己的收集器:

Collector<BigDecimal, BigDecimal[], BigDecimal> avgCollector = Collector.of(
      () -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO},
      (pair, val) -> {
        pair[0] = pair[0].add(val);
        pair[1] = pair[1].add(BigDecimal.ONE);
      },
      (pair1, pair2) -> new BigDecimal[]{pair1[0].add(pair2[0]), pair1[1].add(pair2[1])},
      (pair) -> pair[0].divide(pair[1], 2, RoundingMode.HALF_UP)
);

...然后使用它:

Map<String, BigDecimal> totalProductPriceInEachCategory = shopping.values().stream()
      .flatMap(e -> e.keySet().stream())
      .collect(groupingBy(Product::getCategory, mapping(Product::getPrice, avgCollector)));

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM