BigDecimal你真的会用吗?

1. BigDecimal

1.1 背景

一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多时候因为不知道、不了解或使用不当导致资损事件发生。

所以,如果从事金融相关项目,或者在项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal。

1.2 BigDecimal概述

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。

一般情况下,对于不需要准确计算精度的数字,可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以如果需要精确计算的结果,则必须使用BigDecimal类来操作。

BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。

1.3 为什么使用BigDecimal

看如下代码:

public static void main(String[] args) throws Exception {
        System.out.println(0.2 + 0.1);
        System.out.println(0.3 - 0.1);
        System.out.println(0.2 * 0.1);
        System.out.println(0.3 / 0.1);
}

打印结果:

为何会这样:

原因:不论是float 还是double都是浮点数,而计算机是二进制的,浮点数会失去一定的精确度。

根本原因:十进制值通常没有完全相同的二进制表示形式;十进制数的二进制表示形式可能不精确。只能无限接近于那个值。

但是,在项目中,我们不可能让这种情况出现,特别是金融项目,因为涉及金额的计算都必须十分精确,否则,如果你的微信账户余额显示193.99999999999998,那是一种怎么样的体验?

float 和double为什么不精确:

首先,计算机是只认识二进制的,即 0 和 1,这个大家一定都知道。

那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。

十进制整数转成二进制很简单,通常采用 ”除 2 取余,逆序排列” 即可,如 10 的二进制为 1010

但是,小数的二进制如何表示呢?

十进制小数转成二进制,一般采用 ”乘 2 取整,顺序排列”方法 ,如 0.625 转成二进制的表示为 0.101。

具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的整数部分为零,或者整数部分为1,此时0或1为二进制的最后一位。或者达到所要求的精度为止

但是,并不是所有小数都能转成二进制,如 0.1 就不能直接用二进制表示,他的二进制是 0.000110011001100… 一个无限循环小数

这里我们使用乘2 取整,顺序排列的方法来计算一下

0.1 * 2 = 0.2   -----> 取整数部分0
0.2 * 2 = 0.4   -----> 取整数部分0
0.4 * 2 = 0.8   -----> 取整数部分0
0.8 * 2 = 1.6   -----> 取整数部分1
0.6 * 2 = 1.2   -----> 取整数部分1
0.2 * 2 = 0.2    -----> 取整数部分0 (从此处又开始新的一轮循环)
所以 0.1 的二进制 为 0.0001100110011 无限循

所以,计算机是没办法用二进制精确的表示 0.1 的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来,这也就是为什么float 和double的值并不是精确的原因了,其只是在一定精度范围内表示一个浮点数。

1.4 如何使用BigDecimal

1.4.1 初始化赋值

方法

类型

描述

public BigDecimal(int val)

构造函数

int类型的值生成BigDecimal对象

public BigDecimal(long val)

构造函数

long类型的值生成BigDecimal对象

public BigDecimal(String val)

静态方法

String类型的值转换为BigDecimal类型

public static BigDecimal valueOf(double val)

静态方法

double类型的值转换为BigDecimal类型

public static BigDecimal valueOf(long val)

静态方法

long类型(包含int类型)的值转换为BigDecimal类型

通常建议优先使用String构造方法。当double必须用作BigDecimal的源时,请使用Double.toString(double)转成String,然后使用String构造方法,或使用BigDecimal的静态方法valueOf:

BigDecimal b = new BigDecimal("12.3");
BigDecimal c = BigDecimal.valueOf(7.34325);
BigDecimal d = new BigDecimal(Double.toString(7.34325));
System.out.println(b);
System.out.println(c);
System.out.println(d);

打印结果:

1.4.2 加减乘除运算

运算法则

对应方法

加法

public BigDecimal add(BigDecimal value)

减法

public BigDecimal subtract(BigDecimal value)

乘法

public BigDecimal multiply(BigDecimal value)

除法

public BigDecimal pide(BigDecimal value)

代码示例:

public static void main(String[] args) throws Exception {
    System.out.println("计算加法: " + BigDecimal.valueOf(1.9).add(BigDecimal.valueOf(0.2)));
    System.out.println("计算减法: " + BigDecimal.valueOf(1.9).subtract(BigDecimal.valueOf(1.5)));
    System.out.println("计算乘法: " + BigDecimal.valueOf(1.9).multiply(BigDecimal.valueOf(0.2)));
    System.out.println("计算除法: " + BigDecimal.valueOf(1.9).pide(BigDecimal.valueOf(0.2)));
}

打印结果:

注意点:

  1. BigDecimal的运算结果都是返回了一个新的BigDecimal对象,并不是在原有的对象上进行操作。
  2. 使用pide除法函数除不尽,出现无线循环小数的时候,就需要使用另外精确的小数位数以及舍入模式,不然会出现报错。

1.4.3 保留小数及舍入模式

public BigDecimal setScale(int newScale, int roundingMode)

用于格式化小数的方法,第一个值表示保留几位小数,第二个值表示格式化的类型。

8种类型如下:

格式化类型

描述

ROUND_DOWN

舍弃多余位数,如1.55会格式化为1.5,-1.55会格式化为-1.5

ROUND_UP

进位处理,如1.52会格式化为1.6,-1.52会格式化为-1.6

ROUND_HALF_UP

四舍五入,如果舍弃部分>= .5,则进位

ROUND_HALF_DOWN

五舍六入,如果舍弃部分> .5,则进位

ROUND_CEILING

正无穷大方向舍入模式。如果值为正数,则与ROUND_UP模式相同;如果值为负数,则与ROUND_DOWN模式相同

ROUND_FLOOR

负无穷大方向舍入模式。如果值为正数,则与ROUND_DOWN模式相同;如果值为负数,则与ROUND_UP模式相同

ROUND_UNNECESSARY

确认值的小数位数是否与传入第一个参数(保留小数的位数)相等,如果符合则返回值,如果不符抛出异常

ROUND_HALF_EVEN

如果舍弃部门左边的数字为奇数,则与ROUND_HALF_UP模式相同,如果为偶数则与ROUND_HALF_DOWN模式相同

代码示例:

public static void main(String[] args)  {
    BigDecimal a = BigDecimal.valueOf(5.445);
    System.out.println("5.445舍弃多余位数:" + a.setScale(2, BigDecimal.ROUND_DOWN));
    System.out.println("5.445进位处理:" + a.setScale(2, BigDecimal.ROUND_UP));
    System.out.println("5.445四舍五入(舍弃部分>= .5,进位):" + a.setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("5.445四舍五入(舍弃部分未> .5,舍弃):" + a.setScale(2, BigDecimal.ROUND_HALF_DOWN));
    System.out.println("5.446四舍五入(舍弃部分> .5,进位):" + BigDecimal.valueOf(5.446).setScale(2, BigDecimal.ROUND_HALF_DOWN));
}

打印结果:

1.4.4 比较大小

public int compareTo(BigDecimal val)

BigDecimal类提供的比较值的方法,注意比较的两个值均不能为空。

a.compareTo(b)得到结果 1, 0, -1。

比较结果

描述

1

a 大于b

0

a 等于b

-1

a 小于b

代码示例:

public static void main(String[] args)  {
    BigDecimal a = BigDecimal.valueOf(1);
    BigDecimal b = BigDecimal.valueOf(2);
    BigDecimal c = BigDecimal.valueOf(1);
    BigDecimal d = BigDecimal.ZERO;
    System.out.println("1和2比较结果:" + a.compareTo(b));
    System.out.println("1和1比较结果:" + a.compareTo(c));
    System.out.println("1和0比较判断:" + (a.compareTo(d) > 0) );
}

打印结果:

注意:实际业务中,不会单纯的比较谁打谁小,而是类似于第三条去判断一个布尔值。

1.4.5 其他方法及常量

代码

类型

描述

BigDecimal.ZERO

常量

初始化一个为0的BigDecimal对象

BigDecimal.ONE

常量

初始化一个为1的BigDecimal对象

BigDecimal.TEN

常量

初始化一个为10的BigDecimal对象

public BigDecimal abs()

方法

求绝对值,不管正数还是负数,都得到正数

public BigDecimal negate()

方法

求相反数,正变负,负变正

public BigDecimal pow(int n)

方法

求乘方,如BigDecimal.valueOf(2).pow(3)的值为8

public BigDecimal max(BigDecimal val)

方法

两值比较,返回最大值

public BigDecimal min(BigDecimal val)

方法

两值比较,返回最小值

1.5 BigDecimal的4个坑

在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。

1.5.1 浮点类型的坑

在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。

比如下面的代码:

  @Test
  public void test0(){
    float a = 1;
    float b = 0.9f;
    System.out.println(a - b);
  }

结果是多少?0.1吗?不是,执行上面代码执行的结果是0.100000024。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况

那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:

  @Test
  public void test1(){
    BigDecimal a = new BigDecimal(0.01);
    BigDecimal b = BigDecimal.valueOf(0.01);
    System.out.println("a = " + a);
    System.out.println("b = " + b);
  }

结果:

a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01

上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。

之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了。

而BigDecimal#valueOf则不同,它的源码实现如下:

public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        return new BigDecimal(Double.toString(val));
    }

在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。

此时就得出一个基本的结论:第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值

1.5.2 浮点精度的坑

如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?

先来看一个示例:

  @Test
  public void test2(){
    BigDecimal a = new BigDecimal("0.01");
    BigDecimal b = new BigDecimal("0.010");
    System.out.println(a.equals(b));
    System.out.println(a.compareTo(b));
  }
@Override
public boolean equals(Object x) {
    if (!(x instanceof BigDecimal))
        return false;
    BigDecimal xDec = (BigDecimal) x;
    if (x == this)
        return true;
    if (scale != xDec.scale)
        return false;
    long s = this.intCompact;
    long xs = xDec.intCompact;
    if (s != INFLATED) {
        if (xs == INFLATED)
            xs = compactValFor(xDec.intVal);
        return xs == s;
    } else if (xs != INFLATED)
        return xs == compactValFor(this.intVal);

    return this.inflated().equals(xDec.inflated());
}

equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。

基本结论:通常情况,如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;如果严格限制精度的比较,那么则可考虑使用equals方法

1.5.3 设置精度的坑

在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:

  @Test
  public void test3(){
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("3.0");
    a.pide(b);
  }

执行上述代码的结果是什么?ArithmeticException异常

总结:如果在除法(pide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。

此时,只需在使用pide方法时指定结果的精度即可:

  @Test
  public void test3(){
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("3.0");
    BigDecimal c = a.pide(b, 2,RoundingMode.HALF_UP);
    System.out.println(c);
  }

执行上述代码,输入结果为0.33。

基本结论:在使用BigDecimal进行(所有)运算时,一定要明确指定精度和舍入模式

1.5.4 三种字符串输出的坑

当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?

先来看看下面的代码:

@Test
public void test4(){
  BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
  System.out.println(a.toString());
}

执行的结果是上述对应的值吗?并不是:3.563453525545672E+16

也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。

这里我们需要了解BigDecimal转换字符串的三个方法

基本结论:根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为toPlainString()

1.5.5 NumberFormat类

另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。

使用示例如下:

NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();  //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小数点最多3位

BigDecimal loanAmount = new BigDecimal("15000.48"); //金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘

System.out.println("金额:	" + currency.format(loanAmount));
System.out.println("利率:	" + percent.format(interestRate));
System.out.println("利息:	" + currency.format(interest));

输出结果如下:

金额:  15,000.48 
利率: 0.8% 
利息:  120.00
展开阅读全文

页面更新:2024-04-16

标签:浮点   小数   整数   精度   精确   对象   类型   模式   代码   方法

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top