news 2026/7/15 4:26:28

【Java 8 新特性】Java 8 BigDecimal 分组聚合与统计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Java 8 新特性】Java 8 BigDecimal 分组聚合与统计

1. Java 8中的BigDecimal分组聚合基础

在金融和电商系统中,我们经常需要处理精确的金额计算。BigDecimal作为Java中处理高精度数值计算的利器,配合Java 8的Stream API,可以轻松实现复杂的数据聚合操作。先来看个电商场景的例子:假设我们需要计算不同商品类别的销售总额。

List<Product> products = Arrays.asList( new Product("手机", "电子产品", new BigDecimal("3999.99")), new Product("笔记本", "电子产品", new BigDecimal("5999.99")), new Product("衬衫", "服装", new BigDecimal("199.99")), new Product("裤子", "服装", new BigDecimal("299.99")) ); // 按商品类别分组求和 Map<String, BigDecimal> categorySum = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.reducing(BigDecimal.ZERO, Product::getPrice, BigDecimal::add) )); categorySum.forEach((k,v) -> System.out.println(k + ": " + v));

这段代码会输出:

服装: 499.98 电子产品: 9999.98

这里用到了Stream API的两个核心方法:

  • groupingBy():按指定字段分组
  • reducing():对分组后的元素进行归约操作

2. 自定义Collector实现高级统计

Java 8原生只提供了对基本类型(int/long/double)的统计Collector,对于BigDecimal我们需要自己实现。下面这个工具类可以解决这个问题:

public class BigDecimalCollectors { // 求和 public static <T> Collector<T, ?, BigDecimal> summingBigDecimal(Function<? super T, BigDecimal> mapper) { return Collector.of( () -> new BigDecimal[1], (a, t) -> a[0] = (a[0] == null ? BigDecimal.ZERO : a[0]).add(mapper.apply(t)), (a, b) -> { a[0] = a[0].add(b[0]); return a; }, a -> a[0] == null ? BigDecimal.ZERO : a[0] ); } // 求平均值(四舍五入,保留2位小数) public static <T> Collector<T, ?, BigDecimal> averagingBigDecimal(Function<? super T, BigDecimal> mapper) { return Collector.of( () -> new BigDecimal[2], // [0]总和 [1]计数 (a, t) -> { a[0] = (a[0] == null ? BigDecimal.ZERO : a[0]).add(mapper.apply(t)); a[1] = (a[1] == null ? BigDecimal.ZERO : a[1]).add(BigDecimal.ONE); }, (a, b) -> { a[0] = a[0].add(b[0]); a[1] = a[1].add(b[1]); return a; }, a -> a[0].divide(a[1], 2, RoundingMode.HALF_UP) ); } }

使用示例:

// 按类别统计销售总额 Map<String, BigDecimal> salesByCategory = products.stream() .collect(Collectors.groupingBy( Product::getCategory, BigDecimalCollectors.summingBigDecimal(Product::getPrice) )); // 按类别统计平均价格 Map<String, BigDecimal> avgPriceByCategory = products.stream() .collect(Collectors.groupingBy( Product::getCategory, BigDecimalCollectors.averagingBigDecimal(Product::getPrice) ));

3. 处理空值和异常情况

实际项目中,我们经常会遇到数据不完整的情况。比如某些商品的price字段可能为null。这时候直接计算会抛出NullPointerException。下面介绍几种处理方式:

方法1:过滤空值

Map<String, BigDecimal> result = products.stream() .filter(p -> p.getPrice() != null) .collect(Collectors.groupingBy(...));

方法2:使用默认值

// 在自定义Collector中处理null public static <T> Collector<T, ?, BigDecimal> summingBigDecimal(Function<? super T, BigDecimal> mapper) { return Collector.of( () -> new BigDecimal[1], (a, t) -> { BigDecimal value = mapper.apply(t); a[0] = (a[0] == null ? BigDecimal.ZERO : a[0]) .add(value == null ? BigDecimal.ZERO : value); }, (a, b) -> { a[0] = a[0].add(b[0]); return a; }, a -> a[0] == null ? BigDecimal.ZERO : a[0] ); }

方法3:使用Optional

Map<String, BigDecimal> result = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.reducing( BigDecimal.ZERO, p -> Optional.ofNullable(p.getPrice()).orElse(BigDecimal.ZERO), BigDecimal::add ) ));

4. 多级分组与复合统计

在更复杂的业务场景中,我们可能需要多级分组统计。比如在电商系统中,我们可能需要按"商品类别+用户等级"两级分组统计:

// 假设User类有getLevel方法返回用户等级 List<Order> orders = ...; // 订单数据 // 两级分组统计 Map<String, Map<String, BigDecimal>> result = orders.stream() .collect(Collectors.groupingBy( o -> o.getProduct().getCategory(), Collectors.groupingBy( o -> o.getUser().getLevel(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) ) )); // 输出结果 result.forEach((category, levelMap) -> { System.out.println("【" + category + "】"); levelMap.forEach((level, sum) -> System.out.println(" " + level + "用户: " + sum)); });

对于需要同时计算多个统计指标的场景(如同时计算总和、平均值、最大值、最小值),我们可以使用Collectors.collectingAndThen

class Stats { BigDecimal sum; BigDecimal avg; BigDecimal max; BigDecimal min; // 构造方法、getter省略 } Map<String, Stats> statsByCategory = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.collectingAndThen( Collectors.toList(), list -> { Stats stats = new Stats(); stats.sum = list.stream() .map(Product::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); stats.avg = stats.sum.divide( new BigDecimal(list.size()), 2, RoundingMode.HALF_UP); stats.max = list.stream() .map(Product::getPrice) .reduce(BigDecimal::max) .orElse(BigDecimal.ZERO); stats.min = list.stream() .map(Product::getPrice) .reduce(BigDecimal::min) .orElse(BigDecimal.ZERO); return stats; } ) ));

5. 性能优化与最佳实践

在处理大数据量时,Stream API的性能问题需要注意。以下是几个优化建议:

  1. 并行流谨慎使用:对于简单的数值计算,并行流可能带来额外开销
// 只有当数据量很大时才考虑并行 BigDecimal sum = bigList.parallelStream() .map(Item::getAmount) .reduce(BigDecimal.ZERO, BigDecimal::add);
  1. 避免重复计算:如果需要多个统计指标,尽量在一次遍历中完成

  2. 使用基本类型:对于确定不会溢出且精度要求不高的场景,可以考虑先用基本类型计算再转换

// 先用double计算再转BigDecimal(慎用) double sum = products.stream() .mapToDouble(p -> p.getPrice().doubleValue()) .sum(); BigDecimal total = BigDecimal.valueOf(sum);
  1. 预分配容器:对于已知大小的集合,预分配可以提升性能
List<BigDecimal> bigList = new ArrayList<>(10000); // 预分配大小
  1. 注意BigDecimal的不可变性:每次运算都会创建新对象,大量计算时可能产生较多临时对象

6. 实际应用案例:销售报表生成

让我们通过一个完整的电商销售报表案例,综合运用前面介绍的技术:

public class SalesReportGenerator { public void generateReport(List<Order> orders) { // 1. 按月份统计销售额 Map<String, BigDecimal> monthlySales = orders.stream() .collect(Collectors.groupingBy( o -> DateTimeFormatter.ofPattern("yyyy-MM").format(o.getOrderDate()), BigDecimalCollectors.summingBigDecimal(Order::getAmount) )); // 2. 按商品类别统计 Map<String, BigDecimal> salesByCategory = orders.stream() .collect(Collectors.groupingBy( o -> o.getProduct().getCategory(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) )); // 3. 按用户等级+商品类别两级分组 Map<String, Map<String, BigDecimal>> salesByLevelAndCategory = orders.stream() .collect(Collectors.groupingBy( o -> o.getUser().getLevel(), Collectors.groupingBy( o -> o.getProduct().getCategory(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) ) )); // 4. 生成报表... } // 支持自定义时间维度的统计 public Map<String, BigDecimal> salesByTimeDimension( List<Order> orders, Function<Order, String> dimensionMapper) { return orders.stream() .collect(Collectors.groupingBy( dimensionMapper, BigDecimalCollectors.summingBigDecimal(Order::getAmount) )); } }

使用示例:

List<Order> orders = orderService.getOrders(lastYear); SalesReportGenerator generator = new SalesReportGenerator(); // 按季度统计 Map<String, BigDecimal> quarterlySales = generator.salesByTimeDimension( orders, o -> "Q" + ((o.getOrderDate().getMonthValue() - 1) / 3 + 1) ); // 按周统计(ISO周) Map<String, BigDecimal> weeklySales = generator.salesByTimeDimension( orders, o -> o.getOrderDate().get(WeekFields.ISO.weekOfYear()) + "周" );

7. 常见问题与解决方案

在实际使用中,我遇到过不少坑,这里分享几个典型问题的解决方法:

问题1:精度丢失

BigDecimal a = new BigDecimal("0.1"); BigDecimal b = new BigDecimal("0.2"); BigDecimal sum = a.add(b); // 0.3(正确) // 错误做法: BigDecimal c = new BigDecimal(0.1); // 精度已丢失 BigDecimal d = new BigDecimal(0.2); BigDecimal wrongSum = c.add(d); // 0.300000000000000016...

解决方案:始终使用String构造BigDecimal,或者使用valueOf方法:

BigDecimal c = BigDecimal.valueOf(0.1); // 内部会转成字符串处理

问题2:除不尽异常

BigDecimal a = new BigDecimal("10"); BigDecimal b = new BigDecimal("3"); BigDecimal result = a.divide(b); // 抛出ArithmeticException

解决方案:指定舍入模式

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

问题3:分组键为null

// 如果getCategory()可能返回null,会抛出NPE Map<String, BigDecimal> map = products.stream() .collect(Collectors.groupingBy( Product::getCategory, Collectors.summingBigDecimal(Product::getPrice) ));

解决方案:使用null-safe的分组键

Map<String, BigDecimal> map = products.stream() .collect(Collectors.groupingBy( p -> Optional.ofNullable(p.getCategory()).orElse("其他"), Collectors.summingBigDecimal(Product::getPrice) ));

问题4:内存溢出:处理超大集合时,可以考虑分批处理

// 分批处理大集合 int batchSize = 10000; for (int i = 0; i < bigList.size(); i += batchSize) { List<Order> batch = bigList.subList(i, Math.min(i + batchSize, bigList.size())); // 处理每个批次... }

8. 扩展应用:财务系统中的金额统计

在财务系统中,金额统计的要求更为严格。下面是一个财务金额统计的工具类实现:

public class FinancialStatsUtils { // 安全求和(处理null值) public static BigDecimal safeSum(Collection<BigDecimal> amounts) { return amounts.stream() .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); } // 加权平均计算 public static BigDecimal weightedAverage( List<BigDecimal> values, List<BigDecimal> weights) { if (values.size() != weights.size()) { throw new IllegalArgumentException("值和权重的数量必须相同"); } BigDecimal sumProduct = BigDecimal.ZERO; BigDecimal sumWeights = BigDecimal.ZERO; for (int i = 0; i < values.size(); i++) { BigDecimal value = values.get(i); BigDecimal weight = weights.get(i); if (value != null && weight != null) { sumProduct = sumProduct.add(value.multiply(weight)); sumWeights = sumWeights.add(weight); } } return sumWeights.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : sumProduct.divide(sumWeights, 6, RoundingMode.HALF_UP); } // 计算增长率(百分比) public static BigDecimal growthRate( BigDecimal current, BigDecimal previous, int scale) { if (previous.compareTo(BigDecimal.ZERO) == 0) { return current.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : null; // 或者抛异常,根据业务需求 } return current.subtract(previous) .divide(previous, scale + 2, RoundingMode.HALF_UP) .multiply(new BigDecimal("100")) .setScale(scale, RoundingMode.HALF_UP); } }

使用示例:

// 计算月度增长率 BigDecimal janSales = new BigDecimal("1000000"); BigDecimal febSales = new BigDecimal("1200000"); BigDecimal growthRate = FinancialStatsUtils.growthRate(febSales, janSales, 2); System.out.println("增长率: " + growthRate + "%"); // 增长率: 20.00%

9. 与数据库聚合的结合使用

在实际项目中,我们经常需要将Java层的聚合与数据库层的聚合结合使用。以下是一个结合JPA的示例:

public interface OrderRepository extends JpaRepository<Order, Long> { // 数据库层聚合:按类别统计销售数量 @Query("SELECT o.product.category as category, COUNT(o) as count " + "FROM Order o GROUP BY o.product.category") List<Object[]> countByCategory(); // Java层聚合:获取明细后在内存中计算 default Map<String, BigDecimal> sumAmountByCategory() { return findAll().stream() .collect(Collectors.groupingBy( o -> o.getProduct().getCategory(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) )); } // 混合方案:数据库过滤,Java聚合 default Map<String, BigDecimal> sumAmountByCategory(Date start, Date end) { return findByOrderDateBetween(start, end).stream() .collect(Collectors.groupingBy( o -> o.getProduct().getCategory(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) )); } List<Order> findByOrderDateBetween(Date start, Date end); }

选择策略

  1. 大数据量简单统计 → 优先数据库聚合
  2. 复杂业务逻辑 → Java层聚合
  3. 中等数据量混合统计 → 数据库过滤+Java聚合

10. 测试与验证

为了保证统计结果的正确性,完善的测试必不可少。以下是一些测试用例的示例:

class BigDecimalStatsTest { @Test void testSummingBigDecimal() { List<Product> products = Arrays.asList( new Product("A", new BigDecimal("100.50")), new Product("B", new BigDecimal("200.75")), new Product("C", new BigDecimal("300.25")) ); BigDecimal sum = products.stream() .collect(BigDecimalCollectors.summingBigDecimal(Product::getPrice)); assertEquals(new BigDecimal("601.50"), sum); } @Test void testSummingWithNullValues() { List<Product> products = Arrays.asList( new Product("A", new BigDecimal("100.50")), new Product("B", null), new Product("C", new BigDecimal("300.25")) ); BigDecimal sum = products.stream() .collect(BigDecimalCollectors.summingBigDecimal(Product::getPrice)); assertEquals(new BigDecimal("400.75"), sum); } @Test void testAveragingBigDecimal() { List<Product> products = Arrays.asList( new Product("A", new BigDecimal("100")), new Product("B", new BigDecimal("200")), new Product("C", new BigDecimal("300")) ); BigDecimal avg = products.stream() .collect(BigDecimalCollectors.averagingBigDecimal(Product::getPrice)); assertEquals(new BigDecimal("200.00"), avg); } @Test void testMultiLevelGrouping() { List<Order> orders = Arrays.asList( new Order(new User("VIP"), new Product("A", "电子"), new BigDecimal("100")), new Order(new User("VIP"), new Product("B", "服装"), new BigDecimal("200")), new Order(new User("普通"), new Product("A", "电子"), new BigDecimal("50")) ); Map<String, Map<String, BigDecimal>> result = orders.stream() .collect(Collectors.groupingBy( o -> o.getUser().getLevel(), Collectors.groupingBy( o -> o.getProduct().getCategory(), BigDecimalCollectors.summingBigDecimal(Order::getAmount) ) )); assertEquals(new BigDecimal("100"), result.get("VIP").get("电子")); assertEquals(new BigDecimal("200"), result.get("VIP").get("服装")); assertEquals(new BigDecimal("50"), result.get("普通").get("电子")); } }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/15 4:26:09

在Ubuntu中探索Bash脚本的自动化魔力

1. 从命令行到脚本&#xff1a;Bash自动化初体验第一次接触Ubuntu终端时&#xff0c;你可能已经学会了用ls查看文件、用cd切换目录这些基础命令。但当你反复输入相同的命令序列时&#xff0c;有没有想过让电脑自动完成这些重复劳动&#xff1f;这就是Bash脚本的用武之地。想象一…

作者头像 李华
网站建设 2026/7/15 4:25:48

基于STM32单片机手环老人跌倒GSM短信GPS北斗定位APP地图设计DIY-T036

本系统由STM32单片机核心板、ADXL345加速度传感器、GSM、GPS模块、指示灯、按键电率蜂鸣器报警及电源组成。1、板子正常情况下是竖直装在口袋里的&#xff08;adxl345模块在下角竖直立着&#xff09;&#xff0c;一旦倾角传感器检测到平放即摔倒&#xff0c;蜂鸣器报警,且摔倒的…

作者头像 李华
网站建设 2026/7/15 4:23:24

汽车摄像头FPD-Link III SerDes设计:DS90UB913Q/914Q硬件与调试指南

1. 项目概述&#xff1a;为什么汽车摄像头需要FPD-Link III&#xff1f;在汽车电子&#xff0c;尤其是高级驾驶辅助系统&#xff08;ADAS&#xff09;和环视摄像头的设计中&#xff0c;工程师们面临一个经典难题&#xff1a;如何将高分辨率图像传感器产生的大量并行数据&#x…

作者头像 李华
网站建设 2026/7/15 4:21:49

ABAP 里有没有 Angular OnInit 对应物,一张跨运行模型的初始化语义地图

在一个 Angular 组件里看到 implements OnInit 时,很多有 Java、TypeScript 或 SAP 背景的开发者会自然地寻找 ABAP 里的同名机制。这个问题不能只回答成 constructor,因为 Angular 的 ngOnInit 并不是普通的对象构造函数。更准确的答案是,ABAP 里存在多种与 OnInit 相似的初…

作者头像 李华