精度计算总结 -- BigDecimal,NumberFormat 其他

一、精度丢失的现象

首先,我们来体验下精度丢失现象,如下:

@Test
public void testDouble(){
    double t = 1.2d;
    System.out.println(t-1);
    System.out.println(t+1);
    System.out.println(t/2);
    System.out.println(t*2);
    System.out.println("================");
    float m = 1.2f;
    float m1 = m -1;
    double m11  = m -1;
    float m2 = m+1;
    double m3 = m+1+0.02;
    float m4 = m/2;
    float m5 = m*2;
    System.out.println("m = "+ m + "\nm1 = "+ m1+ "\nm11 = "+m11+ "\nm2 = "+m2);
    System.out.println("m3 = "+ m3 + "\nm4 = "+ m4+ "\nm5 = "+m5);
    System.out.println("================");
    System.out.println(1.2 -1);
    
    System.out.println("================");
    double number1 = 1;
    System.out.println(number1);
    double number2 = 20.2;
    System.out.println(number2);
    double number3 = 300.03;
    System.out.println(number3);
    
    double rs= number1 + number2 + number3;
    System.out.println(rs);
	
}
**********结果****************
0.19999999999999996
2.2
0.6
2.4
================
m = 1.2
m1 = 0.20000005
m11 = 0.20000004768371582
m2 = 2.2
m3 = 2.220000047683716
m4 = 0.6
m5 = 2.4
================
0.19999999999999996
================
1.0
20.2
300.03
321.22999999999996

将上面的代码编译成class,然后反编译过来:

public void testDouble()
{
  double d1 = 1.2D;
  System.out.println(d1 - 1.0D);
  System.out.println(d1 + 1.0D);
  System.out.println(d1 / 2.0D);
  System.out.println(d1 * 2.0D);
  System.out.println("================");
  float f1 = 1.2F;
  float f2 = f1 - 1.0F;
  double d2 = f1 - 1.0F;
  float f3 = f1 + 1.0F;
  double d3 = f1 + 1.0F + 0.02D;
  float f4 = f1 / 2.0F;
  float f5 = f1 * 2.0F;
  System.out.println("m = " + f1 + "\nm1 = " + f2 + "\nm11 = " + d2 + "\nm2 = " + f3);
  System.out.println("m3 = " + d3 + "\nm4 = " + f4 + "\nm5 = " + f5);
  System.out.println("================");
  System.out.println(0.2D);
  System.out.println(321.22999999999996D);

double number1 = 1.0D;
  System.out.println(number1);
  double number2 = 20.199999999999999D;
  System.out.println(number2);
  double number3 = 300.00299999999999D;
  System.out.println(number3);

  double rs = number1 + number2 + number3;
  System.out.println(rs);
}

可以看出有些数据计算没有精度丢失,而有些却有。 仔细瞧瞧,可以发现,在计算数据位达到高位时就会精度丢失。 减法容易丢失,是由于减法 在位运算时会取反进1 再相加,取反后就涉及高精确位了。

但是,总的原因还是数学表达无法满足所致,参考如下:

从 1 3 \frac{1}{3} 31​ 到 0.333。。。。我们先抛开 Java 不管,来看一下这个问题, 如何用十进制小数表示 1 3 \frac{1}{3} 31​ 。

  1. 首先我们讨论如何借用数轴表示 十进位小数 。如果我们将一个数轴分为 10 等份,100 等份(相当于 10 等份的每个单位区间再分 10 份),1000 等份,等等个相等的线段,则其中每个点对应着 一个十进制小数。
    因此一个十进制小数可以表示为不同精度单位的加权(即乘以系数)和。
    比如 0.12 = 1 10 + 2 100 , 它 对 应 的 点 位 于 区 间 长 为 1 0 − 1 0.12=\frac{1}{10}+\frac{2}{100} ,它对应的点位于区间长为 10^{−1} 0.12=101​+1002​,它对应的点位于区间长为10−1 的第二个区间内(1、10 后),是长为 1 0 − 2 10^{−2} 10−2 的第二个子子区间的右端点。

  1. 一个十进制小数,其后有 n 个数码,它的加权式可以写成
    f = z + a 1 ∗ 1 0 − 1 + a 2 ∗ 1 0 − 2 + a 3 ∗ 1 0 − 3 + . . . + a n ∗ 1 0 − n f=z+{a1}*{10^{-1}}+a2*10^{−2}+a3*10^{−3}+...+an*10^{−n} f=z+a1∗10−1+a2∗10−2+a3∗10−3+...+an∗10−n;
    这里 z 是一整数,而 a 是十分之一,百分之一 等等的数码,其中 a∈[0,1,2,…,9] 。另外,因为 1 0 − n = 1 1 0 n 10^{−n}=\frac{1}{10^n} 10−n=10n1​ ,所以十进位小数都可以写成一个分数的形式。比如
    f = 1.134 = 1 + 1 10 + 3 100 + 4 1000 = 1134 1000 f=1.134=1+\frac{1}{10}+\frac{3}{100}+\frac{4}{1000}=\frac{1134}{1000} f=1.134=1+101​+1003​+10004​=10001134​
    。如果分子和分母有公因子,那么分数还可以进行约分。另一方面,如果分母不是 10 的某次幂的因子(即无法通过将分母乘以一个数得到 10 的某次幂(10,100,1000)来通分),那么这个分数不能表示为十进制小数。

  1. 十进制小数举例:
    1 5 = 2 10 = 0.2 ; 1 250 = 4 1000 = 0.004 \frac{1}{5}=\frac{2}{10}=0.2; \frac{1}{250}=\frac{4}{1000}=0.004 51​=102​=0.2;2501​=10004​=0.004
    而非十进制小数如:1/3,它不能写成 n 位十进制小数的形式,因为
    1 3 = b 1 0 n , 意 味 着 3 b = 1 0 n \frac{1}{3}=\frac{b}{10^n} , 意味着 3b=10^n 31​=10nb​,意味着3b=10n,而这个结果是不成立的。我们根据之前得到的十进制小数的表达形式可以得知,非十进制小数将不会坐落在十进制数轴的任何端点上(如果在端点上那么我们就可以通过加权式来表达它,他也就是十进制小数了)。但是,虽然无法精确的在十进制数轴上表达他,我们却可以采用无穷逼近的思想,去无限的接近它。
    首先将 1/3 通分,1/3=10/30;然后我们在数轴上寻找,最接近它的左端点为
    9/30=3/10,右端点为12/30=4/10,1/3 位于这个区间中的某一点,再进一步,做差 10/30−9/30=1/30 ,这时我们取左端点为 3/10 后距离 1/3 在数轴上剩余的距离,再次进行通分,1/30=10/300,现在最接近他的左端点为 9/300=3/100 ,以此类推,我们取到的左端点将是 3/10,3/100,3/1000,… ,而最后,1/3 就等于这些小的间距的加和(从原点到 13 所在点的距离):
    1 3 = 3 10 + 3 100 + 3 1000 + . . . \frac{1}{3}=\frac{3}{10}+\frac{3}{100}+\frac{3}{1000}+... 31​=103​+1003​+10003​+...
    用十进制小数表示也即:
    1 3 = 0.3 + 0.03 + 0.003 + . . . = 0.333.. \frac{1}{3}=0.3+0.03+0.003+...=0.333.. 31​=0.3+0.03+0.003+...=0.333...
    无限接近但是永远不等于 1/3 。在某种精度下,我们可以得到这样的一个近似值 0.333333333334 。

  1. 二进制小数
    之前我们讨论的是在十进制的情况下,但计算机比较擅长使用二进制来表示数,所以我们接下来考虑如果二进制情况下,是否会出现之前在十进制中出现的无法精确表示的情况。(答案是肯定的,这也是我们的主题所在)
    和十进制下相同,对于二进制,式:
    f = z + a 1 ∗ 2 − 1 + a 2 ∗ 2 − 2 + a 3 ∗ 2 − 3 + . . . + a n ∗ 2 − n f=z+a1*2^{−1}+a2*2^{−2}+a3*2^{−3}+...+an*2^{−n} f=z+a1∗2−1+a2∗2−2+a3∗2−3+...+an∗2−n;
    仍成立,不过基数变为了 2。(z 此时是二进制整数)

  1. 十进制下,我们对数轴每次进行 10 等划分,现在我们对数轴的单位长度每次进行 2 等划分。即:
    在这种情况下,我们举一个简单的存在精度丢失的例子:十进制 0.1 的二进制表达。由于0.1=1101;
    在这里我们要求分母必须为 2 的某次幂(2,4,8,16,…)的因子,而 0.1 的 分母为 10 ,显然不符合要求。所以无法用二进制小数精确表示,只能近似。
    近似的过程如下:
    与 0.1 最为接近的左端点是1/16=0.0625,右端点为 18=0.125 ,0.1 在这个区间中的某一点上。做差:0.1−0.0625=0.0375;
    最接近的左端点为: 1 32 = 0.03125 \frac{1}{32}=0.03125 321​=0.03125,如此往复,最终我们得到这样一个式子:
    0.1 = 1 16 + 1 32 + 1 256 + . . . 0.1=\frac{1}{16}+\frac{1}{32}+\frac{1}{256}+... 0.1=161​+321​+2561​+... 该式子只会无限接近 0.1 但是不会等于他。

二、如何解决精度丢失问题

1、BigDecimal 处理

public void testBigDecimal(){
    BigDecimal b1 = new BigDecimal(765+"");
    BigDecimal b2 = new BigDecimal(3518465+"");
    System.out.println(b1.divide(b2,5,BigDecimal.ROUND_HALF_UP));
    System.out.println(b1.divide(b2,5,BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100)));
    System.out.println(b1.divide(b2,15,BigDecimal.ROUND_HALF_UP));
    System.out.println(b1.divide(b2));
}
========结果=======
0.00022
0.02200
0.000217424359771

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
.....
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:220)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:53)

若采用 BigDecimal 计算的结果就必须有个精确值。 遇到无法整除的情况,一定需要配上 有效数字这个选项!!!!

2、 NumberFormat 处理

double number1 = 1;
System.out.println(number1);
 double number2 = 20.2;
 System.out.println(number2);
 double number3 = 300.03;
 System.out.println(number3);

 double result1 = number1 + number2 + number3;
 System.out.println(result1);
 NumberFormat format  = NumberFormat.getInstance();
 System.out.println(format.format(result1));
============================
1.0
20.2
300.03
321.22999999999996
321.23

用NumberFormat类来格式化计算结果,按照自己想要的结果进行格式化,缺点就是要手动去格式化,舍入方式不同结果不一定精确。

3、第三方工具类

double number1 = 1;
System.out.println(number1);
double number2 = 20.2;
System.out.println(number2);
double number3 = 300.03;
System.out.println(number3);

double result1 = number1 + number2 + number3;
System.out.println(result1);
Fraction fraction = Fraction.getFraction(result1);
System.out.println(fraction.doubleValue());

===========================
1.0
20.2
300.03
321.22999999999996
321.23

apache的commons-lang工具包中个math包,提供了常用的计算方法,数字处理方法,如果项目中涉及到的计算比较多,可以考虑使用commons-math包,这个包过于专业化,估计咱们用到的地方不多。
apache的commons-lang的math包,主要有4大类功能

  1. 处理分数的Fraction类,分数表示数字,更为精确
  2. 处理数值的NumberUtils类;这个比较简单,看看api就可以。封装了一些常用数字操作方法,如数字转换、比较,获取一个数字数组中最大值,最小值等。
  3. 处理数值范围的Range、NumberRange、IntRange、LongRange、FloatRange、DoubleRange类;
  4. 处理随机数的JVMRandom和RandomUtils类。这个也比较简单,看看api就可以,获取随机数时比较方便而已。

三、总结

普通的计算最好使用BigDecimal类,这个类可以非常精确的计算出结果,而且你可以完全控制精度,不用额外其他操作,而且与基本类型转换都非常方便。

上一篇:BigDecimal


下一篇:零基础java自学流程-Java语言进阶72