BigDecimal

小数计算丢失精度问题

在计算机中,所有文件都是以二进制存储的,数字运算也是使用二进制进行计算的,因为计算机中不存在小数点,所以我们通常说的浮点数如floatdouble都是计算机使用二进制模拟出来的,但我们在计算机中运行以下代码获得的结果并不是正确的。

double a = 0.1;
double b = 0.3;
System.out.println(b-a); // 结果为0.19999999999999998

为什么呢?

我们先来看十进制小数如何转化为二进制数的

二进制小数 0.1111

第一位1表示十进制1/2
第二位1表示十进制1/4
第三位1表示十进制1/8
第四位1表示十进制1/16
......

所以十进制小数转二进制小数,算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数

0.9*2=1.8   取整数部分 1
0.8(1.8的小数部分)*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 0
......... 0.9二进制表示为(从上往下): 1100100100100......

注意:上面的计算过程循环了,也就是说*2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的 。其实道理很简单,十进制系统中能不能准确表示出1/3呢?同样二进制系统也无法准确表示1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。

BigDecimal简介

因为丢失精度问题,所以java引入了BigDecimal类来解决这一问题。

BigDecimal 由任意精度的整数非标度值 和32 位的整数标度 (scale) 组成。如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负scale 次幂。因此,BigDecimal表示的数值是(unscaledValue × 10-scale)。

BigDecimal函数介绍

构造函数

构造函数如下

我们使用参数为double的构造方法创建对象,并打印输出代码如下

BigDecimal b1 =new BigDecimal(0.1);
System.out.println(b1); //结果为0.1000000000000000055511151231257827021181583404541015625

我们期望的结果为0.1,可是结果还是损失精度的。难道BigDecimal没用吗?

使用下面方式声明BigDecimal时,会出现精度问题

BigDecimal b = new BigDecimal(0.1);

推荐用法

BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = BigDecimal.valueOf(0.1);

用以上方法就不会出问题精度问题

BigDecimal.valueOf() 查看源码就可以知道,也是使用的new BigDecimal("0.1")构造函数

JDK的描述:1、参数类型为double的构造方法的结果有一定的不可预知性。有人可能认为在Java中写入new BigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)。这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)。

2、另一方面,String 构造方法是完全可预知的:写入 newBigDecimal("0.1") 将创建一个 BigDecimal,它正好等于预期的 0.1。因此,比较而言,通常建议优先使用String构造方法。

3、当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。要获取该结果,请使用static valueOf(double)方法。

常用方法

  • 1.BigDecimal(String val):构造方法,将String类型转换成BigDecimal类型数据。
  • 2.BigDecimal(double val):构造方法,将double类型转换成BigDecimal类型数据。
  • 3.BigDecimal(int val):构造方法,将int类型转换成BigDecimal类型数据。
  • 4.BigDecimal add(BigDecimal value):加法,求两个BigDecimal类型数据的和。
  • 5.BigDecimal subtract(BigDecimal value):减法,求两个BigDecimal类型数据的差。
  • 6.BigDecimal multiply(BigDecimal  value):乘法,求两个BigDecimal类型数据的积。
  • 7.BigDecimal divide(BigDecimal divisor):除法,求两个BigDecimal类型数据的商。
  • 8.BigDecimal remainder(BigDecimal divisor):求余数,求BigDecimal类型数据除以divisor的余数。
  • 9.BigDecimal max(BigDecimal value):最大数,求两个BigDecimal类型数据的最大值。
  • 10.BigDecimal min(BigDecimal value):最小数,求两个BigDecimal类型数据的最小值。
  • 11.BigDecimal abs():绝对值,求BigDecimal类型数据的绝对值。
  • 12.BigDecimal negate():相反数,求BigDecimal类型数据的相反数。

代码演示

        BigDecimal  a=new BigDecimal ("4.5");
BigDecimal b=new BigDecimal ("1.5");
BigDecimal c=new BigDecimal ("-10.5"); BigDecimal add_result=a.add(b);
BigDecimal subtract_result=a.subtract(b);
BigDecimal multiply_result=a.multiply(b);
BigDecimal divide_result=a.divide(b);
BigDecimal remainder_result=a.remainder(b);
BigDecimal max_result=a.max(b);
BigDecimal min_result=a.min(b);
BigDecimal abs_result=c.abs();
BigDecimal negate_result=a.negate(); Log.d("TAG","4.5+1.5="+add_result);
Log.d("TAG","4.5-1.5="+subtract_result);
Log.d("TAG","4.5*1.5="+multiply_result);
Log.d("TAG","4.5/1.5="+divide_result);
Log.d("TAG","4.5/1.5余数="+remainder_result);
Log.d("TAG","4.5和1.5最大数="+max_result);
Log.d("TAG","4.5和1.5最小数="+min_result);
Log.d("TAG","-10.5的绝对值="+abs_result);
Log.d("TAG","4.5的相反数="+negate_result);

结果为

4.5+1.5=6.0
4.5-1.5=3.0
4.5*1.5=6.75
4.5/1.5=3
4.5/1.5余数=0.0
4.5和1.5最大数=4.5
4.5和1.5最小数=1.5
-10.5的绝对值=10.5
4.5的相反数=-4.5

BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。

BigDecimalscale()表示小数位数,例如:

BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
System.out.println(d1.scale()); // 2,两位小数
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0

通过BigDecimalstripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal

BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因为去掉了00 BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d1.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2

如果一个BigDecimalscale()返回负数,例如,-2,表示这个数是个整数,并且末尾有2个0。

可以对一个BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:

public class Main {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
System.out.println(d2);
System.out.println(d3);
}
}

BigDecimal做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:

BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入

在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等:

BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为2
System.out.println(d1.compareTo(d2)); // 0

必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。

总是使用compareTo()比较两个BigDecimal的值,不要使用equals()

如果查看BigDecimal的源码,可以发现,实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数:

public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}

BigDecimal也是从Number继承的,也是不可变对象。

源码分析

为什么BigDecimal使用String不会出现精度问题

现在我们明白为什么用double,float 出现出精度问题。现在我们要看一下BigDecimal对String做了什么不会出现精度问题

通过Debug构造方法,我们可以了解,BigDecimal底层数据结构主要是由下面四个属性值组成.

int scale; //有多少位小数(即小数点后有多少位)
int precision; //总工有多少位数字
long intCompact; //字符串去掉小数点后,转为long的值,只有当传的字符串长度小于18时才使用该言
BigInteger intVal; //当传的字符串长度大于等于18时才使用BigInteger表示数字

new BigDecimal("12.12")为例

scale值为2
precision值为4
intCompact值为1212
intVal值为空。
之所以为空是因为字符串长度没有超18位,所以不启用BigInteger表示

看到这进而其实大家应该明白了,BigDecimal将String转为了long或BigInteger来进行计算。

valueOf(doubleval)方法

valueOf实际上是调用相应包装类的toString方法然后使用BigDecimal的参数为String的构造方法创建对象。

add(BigDecimal augend)方法

    public BigDecimal add(BigDecimal augend) {
          long xs = this.intCompact; //整型数字表示的BigDecimal,例a的intCompact值为122
          long ys = augend.intCompact;//同上
BigInteger fst = (this.intCompact != INFLATED) ? null : this.intVal;//初始化BigInteger的值,intVal为BigDecimal的一个BigInteger类型的属性
          BigInteger snd = (augend.intCompact != INFLATED) ? null : augend.intVal;
          int rscale = this.scale;//小数位数
          long sdiff = (long) rscale - augend.scale;//小数位数之差
          if (sdiff != 0) {//取小数位数多的为结果的小数位数
              if (sdiff < 0) {
                 int raise = checkScale(-sdiff);
                 rscale = augend.scale;
                 if (xs == INFLATED ||
                     (xs = longMultiplyPowerTen(xs, raise)) == INFLATED)
                     fst = bigMultiplyPowerTen(raise);
               } else {
                   int raise = augend.checkScale(sdiff);
                   if (ys == INFLATED || (ys = longMultiplyPowerTen(ys, raise)) == INFLATED)
                      snd = augend.bigMultiplyPowerTen(raise);
               }
          }
          if (xs != INFLATED && ys != INFLATED) {
              long sum = xs + ys;
              if ((((sum ^ xs) & (sum ^ ys))) >= 0L)//判断有无溢出
                 return BigDecimal.valueOf(sum, rscale);//返回使用BigDecimal的静态工厂方法得到的BigDecimal实例
           }
           if (fst == null)
               fst = BigInteger.valueOf(xs);//BigInteger的静态工厂方法
           if (snd == null)
               snd = BigInteger.valueOf(ys);
           BigInteger sum = fst.add(snd);
           return (fst.signum == snd.signum) ? new BigDecimal(sum, INFLATED, rscale, 0) :
              new BigDecimal(sum, compactValFor(sum), rscale, 0);//返回通过其他构造方法得到的BigDecimal对象
      }

以上只是对加法源码的分析,减乘除其实最终都返回的是一个新的BigDecimal对象,因为BigInteger与BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以a.add(b);虽然做了加法操作,但是a并没有保存加操作后的值,正确的用法应该是a=a.add(b);

总结

(1)商业计算使用BigDecimal。

(2)尽量使用参数类型为String的构造函数。

(3) BigDecimal都是不可变的(immutable)的,在进行每一步运算时,都会产生一个新的对象,所以在做加减乘除运算时千万要保存操作后的值。

(4)我们往往容易忽略JDK底层的一些实现细节,导致出现错误,需要多加注意。

参考内容:

赵勇文 ,

jackiehff ,

廖雪峰的官方网站

java常用类之BigDecimal的更多相关文章

  1. 【Java常用类】BigDecimal

    BigDecimal 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中, 要求数字精度比较高,故用到java.math.BigDecimal类. BigDecimal类支 ...

  2. Java基础 —— Java常用类

    Java常用类: java.lang包: java.lang.Object类: hashcode()方法:返回一段整型的哈希码,代表地址. toString()方法:返回父类名+"@&quo ...

  3. Java常用类学习笔记总结

    Java常用类 java.lang.String类的使用 1.概述 String:字符串,使用一对""引起来表示. 1.String声明为final的,不可被继承 2.String ...

  4. Java 常用类总结(SE基础)

    本篇博客对java常用类相关知识进行了归纳总结,比较详细,适用于学习和复习. 1. 字符串相关的类 1.1 String String是一个final类,代表不可变的字符序列.不可被继承. Strin ...

  5. Java常用类之要点总结

    Java常用类之要点总结

  6. Java常用类:包装类,String,日期类,Math,File,枚举类

    Java常用类:包装类,String,日期类,Math,File,枚举类

  7. Java常用类的使用

    Java常用类 1. Optional 在我们的开发中,NullPointerException可谓是随时随处可见,为了避免空指针异常,我们常常需要进行 一 些防御式的检查,所以在代码中常常可见if( ...

  8. java常用类详细介绍及总结:字符串相关类、日期时间API、比较器接口、System、Math、BigInteger与BigDecimal

    一.字符串相关的类 1.String及常用方法 1.1 String的特性 String:字符串,使用一对""引起来表示. String声明为final的,不可被继承 String ...

  9. 第十四章 Java常用类

    14.常用类 14.1 字符串相关的类 1课时 14.2 JDK 8之前时间日期API 1课时 14.3 JDK8中新时间日期API 1课时 14.4 JDK8中的Optional类 1课时 14.5 ...

随机推荐

  1. C++为什么不可以把一个数组直接赋值给另一个数组

    今天好奇一个问题, int a[3] = {1,2,3]; int b[3]; b=a; 编译器报错, 网上找了一圈, 大概明白: C++就是如此设定的, 数组不能直接赋值, 可以使用std::cop ...

  2. Git远程新建分支之后,本地看不到最新分支

    git remote update origin –prune 注意 双- csdn 对于 两个 - 看起来像一个

  3. 1.关于python 的hmac加密

    import base64 import hmac import urllib from hashlib import sha1 expires = b" # 过期时间戳 uuid = 'a ...

  4. XML to HTML

    本章讲解如何把 XML 数据显示为 HTML. 在 HTML 中显示 XML 数据 在上一节中,我们讲解了如何通过 JavaScript 来解析 XML 并访问 DOM. 本例遍历一个 XML 文件 ...

  5. XML 验证

    拥有正确语法的 XML 被称为“形式良好”的 XML. 通过 DTD 验证的 XML 是“合法”的 XML. 形式良好的 XML 文档 “形式良好”或“结构良好”的 XML 文档拥有正确的语法. “形 ...

  6. 通过web传大文件

    上传文件的jsp中的部分 通过form表单向后端发送请求 <form id="postForm" action="${pageContext.request.con ...

  7. 洛谷 P4151 BZOJ 2115 [WC2011]最大XOR和路径

    //bzoj上的题面太丑了,导致VJ的题面也很丑,于是这题用洛谷的题面 题面描述 XOR(异或)是一种二元逻辑运算,其运算结果当且仅当两个输入的布尔值不相等时才为真,否则为假. XOR 运算的真值表如 ...

  8. 关于kafka在windows上的安装、运行

    一.安装kafka    下载地址:http://kafka.apache.org/downloads 要下载Binary downloads这个类型,不要下载源文件,这种方便使用.下载后,解压放在D ...

  9. java后台转json、转对象、转list集合

    前台数据传递到后台转json 1.普通格式转换成对象 String data=request.getParameter("data"); //单数据的时候转换方式 JSONObje ...

  10. 微服务SpringCloud系列

    https://my.oschina.net/hmilyylimh?tab=newest&catalogId=5703366