通过自定义收集器解决Collectors.summingDouble计算精度丢失问题
1、自定义收集器Collectorreturn Collector . of(//1、结果容器() -> new BigDecimal [ 1 ] , //2、累加器(result , item) -> {if(item!= 0) {= null) {} else {} } } , //3、并行计算时的合并器(result1 , result2) -> {} , //4、结果转换 total ->
一、问题描述
Java8中提供Stream流式计算和Lambda表达式,极大的简化了对集合对象的一些处理操作。但通过Stream流式计算对Double浮点类型的数据进行计算时,经常会出现精度丢失的问题。
@Test
public void testDoubleSum() {
List<Double> list = Arrays.asList(6.6, 1.3);
double result = list.stream().mapToDouble(Double::new).sum();
System.out.println("mapToDouble(Double::new).sum的值:" + result);
result =list.stream().collect(Collectors.summingDouble(Double::new));
System.out.println("Collectors.summingDouble的值:" + result);
result = list.stream().reduce((a,b)-> a + b).get();
System.out.println("reduce方式的值:" + result);
}
计算结果为:
mapToDouble(Double::new).sum的值:7.8999999999999995
Collectors.summingDouble的值:7.8999999999999995
reduce方式的值:7.8999999999999995
二、原因分析:
上面的三种写法,本质上都是直接对double浮点数据类型进行计算,而在Java中对浮点数进行计算的时候,计算机会先将会先将输入的十进制的 double 类型的数据转换为二进制数据,然后再进行相关的运算。
然而在十进制转二进制的过程中,有些十进制数是无法使用一个有限的二进制数来表达的
,换言之就是转换的时候出现了精度的丢失问题,所以导致最后在运算的过程中,自然就出现了我们看到的一幕。
Collectors.summingDouble()的源码分析:
其计算逻辑是:直接采用double数据进行累加运算
public static <T> Collector<T, ?, Double>
summingDouble(ToDoubleFunction<? super T> mapper) {
/*
* In the arrays allocated for the collect operation, index 0
* holds the high-order bits of the running sum, index 1 holds
* the low-order bits of the sum computed via compensated
* summation, and index 2 holds the simple sum used to compute
* the proper result if the stream contains infinite values of
* the same sign.
*/
return new CollectorImpl<>(
() -> new double[3],
//直接通过double进行累加运算
(a, t) -> { sumWithCompensation(a, mapper.applyAsDouble(t));
a[2] += mapper.applyAsDouble(t);},
(a, b) -> { sumWithCompensation(a, b[0]);
a[2] += b[2];
return sumWithCompensation(a, b[1]); },
a -> computeFinalSum(a),
CH_NOID);
}
三、解决方案:
Java 语言中最经典的便是使用 BigDecimal 来解决。
整体思路是先将 double 类型的数据转换成 BigDecimal 来进行运算,最后再转换成 double 类型的数据。
方式一:将集合中Double对象转为BigDecimal对象在进行计算
@Test
public void testDoubleSum2() {
List<Double> list = Arrays.asList(6.6, 1.3);
//写法一
double result = list.stream().map(e->new BigDecimal(String.valueOf(e))).reduce(BigDecimal::add).get().doubleValue();
System.out.println("先转BigDecimal再采用reduce方式的值:" + result);
//写法二
result = list.stream().map(e->new BigDecimal(String.valueOf(e))).collect(Collectors.reducing(BigDecimal.ZERO, BigDecimal::add)).doubleValue();
System.out.println("Collectors.reducing计算的值:" + result);
}
方式二:自定义收集器Collector处理double计算
1、自定义收集器Collector
public static Collector<Double, ?, Double> summingDouble() {
return Collector.of(
//1、结果容器
()-> new BigDecimal[1],
//2、累加器
(result, item) -> {
if(item != null && item !=0){
if(result[0] != null){
result[0] = result[0].add(new BigDecimal(String.valueOf(item)));
}else{
result[0] = new BigDecimal(String.valueOf(item));
}
}
},
//3、并行计算时的合并器
(result1, result2) -> {
result1[0] = result1[0].add(result2[0]);
return result1;
},
//4、结果转换
total -> total[0].doubleValue()
);
}
2、通过自定义收集器计算
@Test
public void testDoubleSum3() {
List<Double> list = Arrays.asList(6.6, 1.3);
double result = list.stream().collect(summingDouble());
System.out.println("通过自定义收集器summingDouble计算的值:" + result);
}
3、计算结果
通过自定义收集器summingDouble计算的值:7.9
四、补充介绍:如何自定义收集器Collector
可用使用Collector.of()静态方法创建自定义Collector。
不仅是为了更精简和增强可读性,还因为这种方法可以忽略部分不必要的实现。实际上,Collector接口仅需要4个必须部分————提供者(supplier), 累加器(accumulator) ,合并器(combiner)以及类型转换器。
Collector接口说明:
public interface Collector<T, A, R> {
/**
* 提供者:创建并返回新的可变结果容器的函数.
*/
Supplier<A> supplier();
/**
* 累加器:将值累加到结果容器中
*/
BiConsumer<A, T> accumulator();
/**
* 合并器:在并行环境下,流被分为多个部分,每个部分被并行累加
*/
BinaryOperator<A> combiner();
/**
* 结尾函数:执行从中间累积类型A到最终结果类型R的最终转换。
*/
Function<A, R> finisher();
/**
* 特征集:指示Collector属性的特征,可用于优化缩减实现。
*/
Set<Characteristics> characteristics();
1、结果容器提供者(supplier)
实现Collector,必须提供结果容器,即累加值存储的地方,需要注意数组元素的类型,这里我们使用的BigDecimal类型,只用存一个结果元素,所以数组长度为1。
下面代码提供了结果容器:
() -> new BigDecimal[1]
2、计算逻辑:累加元素(accumulator)
接下来,我们需要创建函数实现增加元素至结果容器。在我们的示例中,需要将元素item依次累加到result结果元素中:
(result, item) -> {
if(item != null && item !=0){
if(result[0] != null){
result[0] = result[0].add(new BigDecimal(String.valueOf(item)));
}else{
result[0] = new BigDecimal(String.valueOf(item));
}
}
}
该函数是Consumer类型,其不返回任何值,仅以可变的方式更新结果容器————即数组中的第一个元素。
3、合并器(combiner)
在reduction序列操作中,提供者(supplier) 和 累加器(accumulator) 已经足够了,但为了能够实现并行操作,我们需要实现一个合并器。合并器(combiner)是定义两个结果如何合并的函数。
在并行环境下,流被分为多个部分,每个部分被并行累加。当所有部分都完成时,结果需要使用合并器函数进行合并。下面请看我们的实现代码:
(result1, result2) -> {
result1[0] = result1[0].add(result2[0]);
return result1;
}
4、结果数据类型转换
增加一个函数,其映射结果容器至我们需要的类型。这里我们仅仅需要数组的第一个元素:
total -> total[0].doubleValue()
5、完整示例
public class CollectorsUtil {
public static <T> Collector<T, ?, Double> summingDouble(ToDoubleFunction<? super T> mapper) {
return Collector.of(
//1、结果容器
()-> new BigDecimal[1],
//2、累加器
(result, item) -> {
if(item != null && mapper.applyAsDouble(item) !=0){
if(result[0] != null){
result[0] = result[0].add(new BigDecimal(String.valueOf(mapper.applyAsDouble(item))));
}else{
result[0] = new BigDecimal(String.valueOf(mapper.applyAsDouble(item)));
}
}
},
//3、合并器
(result1, result2) -> {
result1[0] = result1[0].add(result2[0]);
return result1;
},
//4、结果转换
total -> total[0].doubleValue()
);
}
}
@Test
public void testDoubleSum3() {
List<Double> list = Arrays.asList(6.6, 1.3);
double result = list.stream().collect(CollectorsUtil.summingDouble(Double::new));
System.out.println("通过自定义收集器summingDouble计算的值:" + result);
}
总结
Java中凡是涉及到浮点数计算精度问题,统一推荐转化为BigDecimal类型去进行计算,这样才能尽可能的避免精度丢失的问题。
本文主要是以Java8中的Stream流式计算采用Collectors.summingDouble()进行double求和计算时出现精度丢失为案例,阐述如果通过自定义收集器Collector解决精度丢失的问题。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)