【Java常用类】1-5 BigDecimal 类
§1-5 BigDecimal
类
1-5.1 浮点类型的精度丢失
在基础课程,我们已经提及过浮点类型的存储方式。浮点类型采用近似表示的方式存储,在计算时会因为精度丢失造成计算误差的问题。
下面有一段测试浮点类型的精度丢失的 Java 程序:
public class PrecisionLoss {
public static void main(String[] args) {
double d1 = 1.0;
double d2 = 0.9;
System.out.println(d1 - d2); //是否等于 0.1?
d1 = 0.1;
d2 = 0.2;
System.out.println(d1 + d2); //是否等于0.3?
d1 = 1.4;
d2 = 0.5;
double d3 = 0.9;
System.out.println((d1-d2) / d3); //是否等于1?
}
}
编译后运行:
0.09999999999999998
0.30000000000000004
0.9999999999999999
计算结果发生了精度丢失。同样地,让 Python 计算同样的算式,得到:
>>> 0.1+0.2
0.30000000000000004
>>> 1-0.9
0.09999999999999998
>>> (1.4-0.5)/0.9
0.9999999999999999
这样的精度丢失,是小数在计算机中的表示方法导致的。在一些重要领域中将可能会造成灾难性后果。为防止这种现象的产生,满足精确计算的要求,我们可以使用 BigDecimal
类。
1-5.2 使用 BigDecimal
解决问题
BigDecimal
类位于 java.math
包下,继承自 Number
,主要用于精确计算浮点数,以及很大的小数表示。
使用 BigDecimal
,需要创建对象使用。且由于其不再属于基本数据类型,只能够通过使用内置方法进行计算。
使用 BigDecimal
解决前文所提到的算术问题。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PrecisionLossFix {
public static void main(String[] args) {
BigDecimal bd1 = new BigDecimal("1.0");
BigDecimal bd2 = new BigDecimal("0.9");
BigDecimal res1 = bd1.subtract(bd2);
System.out.println("1.0 - 0.9 = " + res1);
BigDecimal bd3 = new BigDecimal("0.1");
BigDecimal bd4 = new BigDecimal("0.2");
BigDecimal res2 = bd3.add(bd4);
System.out.println("0.1 + 0.2 = " + res2);
BigDecimal res3 = new BigDecimal("1.4")
.subtract(new BigDecimal("1.5"))
.divide(new BigDecimal("0.9"));
System.out.println("(1.4-0.5) / 0.9 = " + res3);
}
}
编译运行,得到
1.0 - 0.9 = 0.1
0.1 + 0.2 = 0.3
(1.4-0.5) / 0.9 = 1
1-5.3 使用 BigDecimal
构造方法:
构造方法 | 描述 |
---|---|
BigDecimal(String val) |
将 BigDecimal 的字符串表示形式转换为 BigDecimal |
BigDecimal(double val) |
将 double 转换为 BigDecimal ,这是 double 的二进制浮点值的精确十进制表示 |
BigDecimal(BigInteger val) |
将 BigInteger 翻译成 BigDecimal |
静态方法:
静态方法 | 描述 |
---|---|
static BigDecimal valueOf(double val) |
使用 Double.toString(double) 方法提供的 double 的规范字符串表示将 double 转换为 BigDecimal |
static BigDecimal valueOf(long val) |
将 long 值转换为标度为零的 BigDecimal |
建议方法:
BigDecimal
具有很多构造方法,建议使用BigDecimal(String val)
方法。若使用浮点数的传参重载,其一定的不可预知性可能导致创建的对象值并不精确等于预想的值;- 使用
BigInteger
的传参重载构造方法,得到的BigDecimal
的scale
为零(即精确到小数点后几位); - 如果使用的数字不超过
double
的范围,建议使用静态方法;若使用的数字超出范围,建议使用构造方法; - 类似于
Integer
缓冲区,对于传入整型的静态重载方法,若传入的参数范围位于 \([0, 10]\),则返回已有对象,否则新建对象;
常用方法:
方法 | 描述 |
---|---|
BigDecimal add(BigDecimal val) |
加法 |
BigDecimal subtract(BigDecimal val) |
减法 |
BigDecimal multiply(BigDecimal val) |
乘法 |
BigDecimal divide(BigDecimal divisor) |
除法 |
BigDecimal divide(BigDecimal divisor, int scale, RoundingMode roundingMode) |
除法,指定保留小数位和舍入模式 |
BigDecimal remainder(BigDecimal val) |
返回余数 |
BigDecimal negate() |
返回相反数,精确值不变 |
BigDecimal pow(int n) |
返回幂,精确计算至无限精度 |
BigDecimal sqrt(MathContext mc) |
返回平方根的近似值,根据上下文设置舍入 |
BigDecimal max/min(BigDecimal val) |
返回最值 |
double doubleValue() |
返回该 BigDecimal 的 double 转换 |
double intValueExact() |
返回该 BigDecimal 的 int 转换,并检查丢失信息 |
boolean equals() |
判断二者数值是否相等 |
int compareTo() |
判断二者数值是否相等,大于返回 1 ,等于返回 0 ,小于返回 -1 |
注意:
- 对于无法整除而造成除不尽的除法,应当使用带有三个参数的重载版本,否则会抛出
ArithmeticException
异常; RoundingMode
枚举常量在下文列出;int intValue()
方法在转换过程中,若BigDecimal
太大而无法放入int
中,则仅返回低位 32 位。此转换可能会丢失有关BigInteger
整体大小的信息,并返回一个带有相反符号的结果。同样地,类似的情况也会发生在doubleValue()
,longValue()
方法中,对于double
,若量级太大而无法表示,则会根据需要转换为double_NEGATIVE_INFINITY
或double_POSITIVE_INFINITY
;int intValueExact()
方法在转换过程中,若转换超出范围,无法放入int
中,则抛出异常ArithmeticException
。类似地,相同的情况也会发生在byteValueExact()
,longValueExact()
等方法中;- 平方根方法中的参数来自于类
MathContext
所定义的枚举常量,在下文列出; - 返回最值时,方法不会创建新的对象,而是返回符合要求的对象。
1-5.4 注意事项
-
注意创建对象时,传入参数必须合法:字符串中不可包含非法字符;
-
构造完成后,对象中的值不可变;
-
BigDecimal
支持一些基本的数学运算,通过方法来包装:四则运算、幂运算、平方根、绝对值、相反数、最值; -
BigDecimal
中的一些常量已被弃用,代之以RoundingMode
下的常量; -
若除法计算无法整除,出现无限小数、计算出
NaN
或精确无穷(例如除以零),则会抛出异常ArithmeticException
; -
RoundingMode
下的常量列表枚举常量 描述 RoudingMode.CEILING
向正无穷舍入。对于正数,行为同 RoundingMode.UP
,对于负数,行为同RoundingMode.DOWN
。注意,该舍入模式不会减小已计算的值。RoundingMode.FLOOR
向负无穷舍入。对于正数,行为同 RoundingMode.DOWN
,对于负数,行为同RoundingMode.UP
。注意,该舍入模式不会增大已计算的值。RoundingMode.HALF_UP
四舍五入,向最近的整数舍入,若有等距临近整数,则向上舍入。若舍弃的小数部分 ≥ 0.5,行为等同 RoundingMode.UP
;否则,行为等同RoundingMode.DOWN
。RoundingMode.HALF_DOWN
向最近的整数舍入,若有等距临近整数,则向下舍入。若舍弃的小数部分 >0.5,行为等同 RoundingMode.UP
;否则,行为等同RoundingMode.DOWN
。RoundingMode.HALF_EVEN
银行家舍入,向最近的整数舍入,若有等距临近整数,向偶数舍入。若整数部分为奇数,行为同 RoundingMode.HALF_UP
;为偶数则同RoundingMode.HALF_DOWN
。RoundingMode.UP
向远离 0 的方向舍入。总是让具有非零舍弃的小数部分的数字递增(increments)。注意,该模式不会减小已计算的值的大小(magnitude)。 RoundingMode.DOWN
向靠近 0 的方向舍入。从不让具有非零舍弃的小数部分的数字递增(截短)。注意,该模式不会增大已计算的值的大小。 RoundingMode.UNNECESSARY
用于判定请求的运算是否具有精确值,因此没有必要做舍入。若运算产生非精确值,将抛出异常 ArithmeticException
。 -
MathContext
中的枚举常量枚举常量 描述 DECIMAL128
其精度设置与 IEEE 754-2019 decimal128 格式的精度、34 位数字和 HALF_EVEN
的舍入模式相匹配。DECIMAL32
其精度设置与 IEEE 754-2019 decimal32 格式的精度、7 位数字和 HALF_EVEN
的舍入模式相匹配。DECIMAL64
其精度设置与 IEEE 754-2019 decimal64 格式的精度、16 位数字和 HALF_EVEN
的舍入模式相匹配。UNLIMITED
其精度设置具有无限精度算术所需的值。
1-5.5 比较数值
在 BigDecimal
类中,定义了两个方法:equals();
和 compareTo();
。
二者的区别是,前者比较的是数值和比例(scale
),仅在而这都相同的情况下返回 true
,而后者只比较数值,小于返回 -1
,等于返回 0
,大于返回 1
。
示例:
public class DecimalComparison {
public static void main(String[] args) {
BigDecimal bd1 = new BigDecimal("2.0");
BigDecimal bd2 = new BigDecimal("2.00");
BigDecimal bd3 = new BigDecimal("1.00");
//比较
System.out.println("equals: " + bd1.equals(bd2));
System.out.println("compare: " + bd1.compareTo(bd2));
System.out.println("compare: " + bd2.compareTo(bd3));
System.out.println("compare: " + bd3.compareTo(bd2));
}
}
得到
equals: false
compare: 0
compare: 1
compare: -1
因此,比较数值大小,我们常用 compareTo()
方法。
1-5.6 底层原理
BigDecimal
的存储原理如下:
以数字 -1.5
为例
-
将传入的数字以字符串的形式遍历,得到每个字符的 ASCII 编码值,存储到
byte
类型数组中;本例,转换得到
45, 49, 46, 53
-
以这种方式存储,可节约空间。
java 中的数组长度上限为 int
上限(约 21 亿),可近似认为 BigDecimal
所能存储的小数接近无限。
1-5.7 格式化
使用 java.text
包下的 NumberFormat
类格式化 BigDecimal
。