老生常谈之慎用 BigDecimal
文章目录
前言
??在项目中发现开发小组成员在写程序时,对于 Oracle 数据类型为 Number 的字段、mysql中的decimal字段,在实体映射类型中,有的人用 Double,有的人用 BigDecimal,没有一个统一规范,为此我在这里总结记录一下。
??BigDecimal,相信对于很多人来说都不陌生,很多人都知道他的用法,这是一种 java.math 包中提供的一种可以用来进行精确运算的类型。很多人都知道,在进行金额表示、金额计算等场景,不能使用 double、float等类型,而是要使用对精度支持的更好的 BigDecimal。所以,很多支付、电商、金融等业务中,BigDecimal 的使用非常频繁。但是,如果误以为只要使用 BigDecimal 表示数字,结果就一定精确,那就大错特错了!关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求:
??这是一条【强制】建议,那么,这背后的原理是什么呢?想要搞清楚这个问题,主要需要弄清楚以下几个问题。在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。
- 为什么说double不精确?
- BigDecimal是如何保证精确的?
一、快速入门
1.1 简介
??Java 在 java.math 包中提供的 API 类 BigDecimal,用来对超过16位有效位的数进行精确的运算,而双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。float和double只能用来做科学计算或者是工程计算,在商业计算中要用 java.math.BigDecimal。BigDecimal 所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。
??在 Java 开发中,我们经常会使用 BigDecimal 来进行精确的数值计算,特别是在涉及货币、金融等领域。BigDecimal 提供了高精度的计算能力,可以避免由于浮点数计算引起的精度丢失问题。然而,在线上环境中,慎用 BigDecimal 是一个需要考虑的问题。
1.2 构造函数
构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象。
构造函数 | 说明 |
---|---|
BigDecimal(int) | 创建一个具有参数所指定整数值的对象 |
BigDecimal(double) | 创建一个具有参数所指定双精度值的对象 |
BigDecimal(long) | 创建一个具有参数所指定长整数值的对象 |
BigDecimal(String) | 创建一个具有参数所指定以字符串表示的数值的对象 |
二、BigDecimal常用方法
2.1 常用方法
方法 | 说明 |
---|---|
add(BigDecimal) | BigDecimal对象中的值相加,返回BigDecimal对象 |
subtract(BigDecimal) | BigDecimal对象中的值相减,返回BigDecimal对象 |
multiply(BigDecimal) | BigDecimal对象中的值相乘,返回BigDecimal对象 |
divide(BigDecimal) | BigDecimal对象中的值相除,返回BigDecimal对象 |
toString() | 将BigDecimal对象的数值转换成字符串 |
doubleValue() | 将BigDecimal对象中的值以双精度数返回 |
floatValue() | 将BigDecimal对象中的值以单精度数返回 |
longValue() | 将BigDecimal对象中的值以长整数返回 |
intValue() | 将BigDecimal对象中的值以整数返回 |
2.2 BigDecimal格式化
??由于 NumberFormat 类的 format() 方法可以使用 BigDecimal 对象作为其参数,可以利用 BigDecimal 对超出16位有效数字的货币值、百分值以及一般数值进行格式化控制。这里利用 BigDecimal 对货币和百分比格式化为例。首先,创建 BigDecimal 对象,进行 BigDecimal 的算术运算后,分别建立对货币和百分比格式化的引用,最后利用 BigDecimal 对象作为 format() 方法的参数,输出其格式化的货币值和百分比。
public class BigDecimalDemo {
public static void main(String[] args) {
// 建立货币格式化引用
NumberFormat currency = NumberFormat.getCurrencyInstance();
// 建立百分比格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();
// 百分比小数点最多3位
percent.setMaximumFractionDigits(3);
//贷款金额
BigDecimal loanAmount = new BigDecimal("15000.48");
//利率
BigDecimal interestRate = new BigDecimal("0.008");
//相乘
BigDecimal interest = loanAmount.multiply(interestRate);
System.out.println("贷款金额:\t" + currency.format(loanAmount));
System.out.println("利率:\t" + percent.format(interestRate));
System.out.println("利息:\t" + currency.format(interest));
}
}
BigDecimal格式化保留2为小数,不足则补0:
public class BigDecimalDemo {
public static void main(String[] args) {
System.out.println(formatToNumber(new BigDecimal("3.435")));
System.out.println(formatToNumber(new BigDecimal(0)));
System.out.println(formatToNumber(new BigDecimal("0.00")));
System.out.println(formatToNumber(new BigDecimal("0.001")));
System.out.println(formatToNumber(new BigDecimal("0.006")));
System.out.println(formatToNumber(new BigDecimal("0.206")));
}
/**
* 1、0~1之间的BigDecimal小数,格式化后失去前面的0,则前面直接加上0。
* 2、传入的参数等于0,则直接返回字符串"0.00"
* 3、大于1的小数,直接格式化返回字符串
*/
public static String formatToNumber(BigDecimal obj) {
DecimalFormat df = new DecimalFormat("#.00");
if (obj.compareTo(BigDecimal.ZERO) == 0) {
return "0.00";
} else if (obj.compareTo(BigDecimal.ZERO) > 0 && obj.compareTo(new BigDecimal(1)) < 0) {
return "0" + df.format(obj).toString();
} else {
return df.format(obj).toString();
}
}
}
三、BigDecimal常见异常
3.1 使用除法时除不尽会报 ArithmeticException
异常
public static void main(String[] args) {
System.out.println(BigDecimal.valueOf(121).divide(BigDecimal.valueOf(3)));
}
??通过BigDecimal的divide方法进行除法时当不整除,出现无限循环小数时,就会抛异常:java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
。
??处理上述异常也很简单,只需要 divide 方法设置精确的小数点,如:divide(xxxxx,2)
public static void main(String[] args) {
System.out.println(BigDecimal.valueOf(121).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP));
}
3.2 比较大小不方便
BigDecimal大小的比较都需要使用compareTo
,如果需要返回更大的数或更小的数可以使用max
、min
。还要注意在BigDecimal中慎用equals
。
public static void main(String[] args) {
BigDecimal a = BigDecimal.valueOf(12.3);
BigDecimal b = BigDecimal.valueOf(12.32);
System.out.println(a.compareTo(b)); // -1
System.out.println(b.compareTo(a)); //1
System.out.println(a.max(b)); // 12.32
System.out.println(a.min(b)); // 12.3
System.out.println(b.max(a)); // 12.32
System.out.println(b.min(a)); // 12.3
System.out.println(BigDecimal.valueOf(1).equals(BigDecimal.valueOf(1.0))); //false
}
??BigDecimal 重写了 equals 方法,在 equals 方法里比较了小数位数,所以 BigDecimal.valueOf(1).equals(BigDecimal.valueOf(1.0))
为什么结果为 false
就可以理解了。在附录中会贴出 BigDecimal 中的 equals
方法的源码。通过源码可以知道 if (scale != xDec.scale)
这句代码就是比较了小数位数,不等则直接返回false
。
四、附录
4.1 工具类推荐
package org.dllwh.oshi;
import java.math.*;
/**
* 把今天最好的表现当作明天最新的起点..~
* <p>
* Today the best performance as tomorrow the newest starter!
*
* @类描述: 用于高精确处理常用的数学运算
* @author: <a href="mailto:duleilewuhen@sina.com">独泪了无痕</a>
* @创建时间: 2024-01-02 23:29
* @版本: V 1.0.1
* @since: JDK 1.8
*/
public class ArithmeticHelper {
// 默认除法运算精度
private static final int DEF_DIV_SCALE = 10;
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static double add(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @return 两个参数的和
*/
public static BigDecimal add(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2);
}
/**
* 提供精确的加法运算
*
* @param v1 被加数
* @param v2 加数
* @param scale 保留scale 位小数
* @return 两个参数的和
*/
public static String add(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.add(b2).setScale(scale, RoundingMode.HALF_UP).toString();
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的减法运算。
*
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static BigDecimal sub(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2);
}
/**
* 提供精确的减法运算
*
* @param v1 被减数
* @param v2 减数
* @param scale 保留scale 位小数
* @return 两个参数的差
*/
public static String sub(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.subtract(b2).setScale(scale, RoundingMode.HALF_UP).toString();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1, double v2) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static BigDecimal mul(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static double mul(double v1, double v2, int scale) {
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return round(b1.multiply(b2).doubleValue(), scale);
}
/**
* 提供精确的乘法运算
*
* @param v1 被乘数
* @param v2 乘数
* @param scale 保留scale 位小数
* @return 两个参数的积
*/
public static String mul(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.multiply(b2).setScale(scale, RoundingMode.HALF_UP).toString();
}
/**
* 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
* 小数点以后10位,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @return 两个参数的商
*/
public static double div(double v1, double v2) {
return div(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示表示需要精确到小数点以后几位。
* @return 两个参数的商
*/
public static double div(double v1, double v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
* 定精度,以后的数字四舍五入
*
* @param v1 被除数
* @param v2 除数
* @param scale 表示需要精确到小数点以后几位
* @return 两个参数的商
*/
public static String div(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v1);
return b1.divide(b2, scale, RoundingMode.HALF_UP).toString();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static double round(double v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException("The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(Double.toString(v));
return b.setScale(scale, RoundingMode.HALF_UP).doubleValue();
}
/**
* 提供精确的小数位四舍五入处理
*
* @param v 需要四舍五入的数字
* @param scale 小数点后保留几位
* @return 四舍五入后的结果
*/
public static String round(String v, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b = new BigDecimal(v);
return b.setScale(scale, RoundingMode.HALF_UP).toString();
}
/**
* 取余数
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static String remainder(String v1, String v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
return b1.remainder(b2).setScale(scale, RoundingMode.HALF_UP).toString();
}
/**
* 取余数 BigDecimal
*
* @param v1 被除数
* @param v2 除数
* @param scale 小数点后保留几位
* @return 余数
*/
public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
if (scale < 0) {
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
return v1.remainder(v2).setScale(scale, RoundingMode.HALF_UP);
}
/**
* 比较大小
*
* @param v1 被比较数
* @param v2 比较数
* @return 如果v1 大于v2 则 返回true 否则false
*/
public static boolean compare(String v1, String v2) {
BigDecimal b1 = new BigDecimal(v1);
BigDecimal b2 = new BigDecimal(v2);
int bj = b1.compareTo(b2);
return bj > 0;
}
}
4.2 BigDecimal(double)有什么问题
??首先,计算机是只认识二进制的,即0和1,这个大家一定都知道。那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。但是,小数的二进制如何表示呢?
??十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。那么,这个问题总要解决吧。那么,人们想出了一种采用一定的精度,使用近似值表示一个小数的办法。这就是IEEE 754(IEEE二进制浮点数算术标准)规范的主要思想。
??IEEE 754规定了多种表示浮点数值的方式,其中最常用的就是32位单精度浮点数和64位双精度浮点数。在Java中,使用float和double分别用来表示单精度浮点数和双精度浮点数。所谓精度不同,可以简单的理解为保留有效位数不同,采用保留有效位数的方式近似的表示小数。所以,大家也就知道为什么double表示的小数不精确了。
??那么BigDecimal又是如何精确计数?如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。在BigDecimal中,标度是通过scale字段来表示的。而无标度值的表示比较复杂。当unscaled value超过阈值(默认为Long.MAX_VALUE)时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE,否则对unscaled value进行压缩存储到long型的intCompact字段用于后续计算,intVal为空。涉及到的字段就是这几个:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
private final transient long intCompact;
}
关于无标度值的压缩机制大家了解即可,不是本文的重点,大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。
??BigDecimal 中提供了一个通过 double 创建 BigDecimal 的方法——BigDecimal(double) ,但是,同时也给我们留了一个坑!因为我们知道,double表示的小数是不精确的,如0.1这个数字,double只能表示他的近似值,这是因为doule自身表示的只是一个近似值。所以,如果我们在代码中,使用BigDecimal(double) 来创建一个BigDecimal的话,那么是损失了精度的,这是极其严重的。
4.3 使用BigDecimal(String)创建
??那么,该如何创建一个精确的BigDecimal来表示小数呢,答案是使用String创建。而对于BigDecimal(String) ,当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。那么他的标度也就是1。但是需要注意的是,new BigDecimal(“0.10000”)和new BigDecimal(“0.1”)这两个数的标度分别是5和1,如果使用BigDecimal的equals方法比较,得到的结果是false。那么,想要创建一个能精确的表示0.1的BigDecimal,请使用以下两种方式:
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
4.4 BigDecimal 的 equals方法源码
/**
* 该方法认为两个BigDecimal对象只有在值和比例相等时才相等,所以当使用该方法比较2.0与2.00时,二者不相等。
*/
public boolean equals(Object x) {
// 比较对象是否为 BigDecimal 数据类型,不是直接返回false
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
// 比较 scale 值是否相等。在这里比较了小数位数,不等返回false。
// scale 是BigDecimal 的标度。如果为零或正数,则标度是小数点后的位数。
// 如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。例如,-3 标度是指非标度值乘以 1000。
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());
}
4.5 关于MySql中如何选用这两种类型
??在查资料的时候还看到了关于MySql
中如何选用这两种类型的问题,也在此记录一下。在数据库中除了指定数据类型之外还需要指定精度,所以在MySql
里Double
的计算精度丢失比在Java
里要高很多,Java
的默认精度到了15-16位。在阿里的编码规范中也强调 统一带小数类型的一律要使用Decimal
类型而不是Double
,使用Decimal
可以大大减少计算采坑的概率。所以在选用类型时,与Java
同样,在精度要求不高的情况下可以使用Double
,比如经纬度,但是有需要计算、金融金额等优先使用Decimal
。
五、小结
??在需要精确的小数计算时再使用 BigDecimal,BigDecimal 的性能比 double 和 float差,在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal,尽量使用参数类型为String的构造函数。
??BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。所以,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。
参考链接:
- https://zhuanlan.zhihu.com/p/94144867
- https://www.cnblogs.com/r1-12king/p/15895512.html
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!