扩展 - 数值计算问题
# 数值计算问题
注意精度、舍入和溢出问题
# 1. “危险”的 Double
public class DoubleTest {
public static void main(String[] args) {
System.out.println(0.1+0.2); // 0.30000000000000004
System.out.println(1.0-0.8); // 0.19999999999999996
System.out.println(4.015*100); // 401.49999999999994
System.out.println(123.3/100); // 1.2329999999999999
double amount1 = 2.15;
double amount2 = 1.10;
if (amount1 - amount2 == 1.05) {
System.out.println("OK");
} else {
System.out.println("NO " + (amount1 - amount2)); // NO 1.0499999999999998
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
计算机是以二进制存储数值的。0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
如果大量使用 double 来作大量的金钱计算,最终损失的精度就是大量的资金出入。比如,每天有一百万次交易,每次交易都差一分钱,一个月下来就差 30 万
# 2. 使用 BigDecimal 类型代替 Double
BigDecimal的几个坑
import java.math.BigDecimal;
public class DecimalTest {
public static void main(String[] args) {
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2))); // 0.3000000000000000166533453693773481063544750213623046875
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8))); // 0.1999999999999999555910790149937383830547332763671875
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100))); // 401.49999999999996802557689079549163579940795898437500
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100))); // 1.232999999999999971578290569595992565155029296875
}
}
2
3
4
5
6
7
8
9
10
运算结果还是不精确,只不过精度高了而已。
计算浮点数避坑第一原则:
- 使用BigDecimal表示和计算浮点数
- 务必使用字符串的构造方法来初始化BigDecimal
import java.math.BigDecimal;
public class DecimalTest {
public static void main(String[] args) {
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2"))); // 0.3
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8"))); // 0.2
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100"))); // 401.500
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100"))); // 1.233
System.out.println(BigDecimal.valueOf(0.1).add(BigDecimal.valueOf(0.2))); // 0.3
System.out.println(BigDecimal.valueOf(1.0).subtract(BigDecimal.valueOf(0.8))); // 0.2
System.out.println(BigDecimal.valueOf(4.015).multiply(BigDecimal.valueOf(100))); // 401.500
System.out.println(BigDecimal.valueOf(123.3).divide(BigDecimal.valueOf(100))); // 1.233
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BigDecimal的scale 和 precision 概念:scale 表示小数点右边的位数;precision表示精度,即有效数字的长度
new BigDecimal(Double.toString(100))
得到的 BigDecimal 的 scale=1、precision=4;
new BigDecimal(“100”)
得到的 BigDecimal 的 scale=0、precision=3
对于 BigDecimal 乘法操作,返回值的 scale 是两个数的 scale 相加
private static void testScale() {
BigDecimal bigDecimal1 = new BigDecimal("100");
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));
print(bigDecimal1); //scale 0 precision 3 result 401.500
print(bigDecimal2); //scale 1 precision 4 result 401.5000
print(bigDecimal3); //scale 0 precision 3 result 401.500
print(bigDecimal4); //scale 1 precision 4 result 401.5000
print(bigDecimal5); //scale 1 precision 4 result 401.5000
}
private static void print(BigDecimal bigDecimal) {
log.info("scale {} precision {} result {}", bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal("4.015")));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 3. 浮点数舍入和格式化的方式
double num1 = 3.35; // 相当于:3.350000000000000088817841970012523233890533447265625
float num2 = 3.35f; // 相当于:3.349999904632568359375
System.out.println(String.format("%.1f", num1));//四舍五入 3.4
System.out.println(String.format("%.1f", num2)); // 3.3
2
3
4
String.format 采用四舍五入的方式进行舍入,取 1 位小数,所以记过为3.4 和 3.3 String.format 采用HALF_UP模式
使用其他舍入方式格式化字符串,可以使用DecimalFormat
double num1 = 3.35; // 相当于:3.350000000000000088817841970012523233890533447265625
float num2 = 3.35f; // 相当于:3.349999904632568359375
DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num1)); // 3.35
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num2)); // 3.34 期望是3.35
2
3
4
5
6
7
8
因此,即使通过 DecimalFormat 来精确控制舍入方式,double 和 float 的问题也可能产生意想不到的结果 所以浮点数避坑第二原则:浮点数的字符串格式化也要通过 BigDecimal 进行。
比如下面这段代码,使用 BigDecimal 来格式化数字 3.35,分别使用向下舍入和四舍五入方式取 1 位小数进行格式化
BigDecimal num1 = new BigDecimal("3.35");
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN); // 3.3
System.out.println(num2);
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP); // 3.4
System.out.println(num3);
2
3
4
5
# 4. 用equals做判等,就一定对吗?
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1"))) // false
equals 比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的 scale 是 0,所以结果一定是 false 如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法
System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1"))==0);
Set<BigDecimal> hashSet1 = new HashSet<>();
hashSet1.add(new BigDecimal("1.0"));
System.out.println(hashSet1.contains(new BigDecimal("1")));//返回false
2
3
解决方法1:使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
System.out.println(treeSet.contains(new BigDecimal("1")));//返回true
2
3
解决方法2:把 BigDecimal 存入 HashSet 或 HashMap 前,先使用 stripTrailingZeros 方法去掉尾部的零,比较的时候也去掉尾部的 0,确保 value 相同的 BigDecimal,scale 也是一致的
Set<BigDecimal> hashSet2 = new HashSet<>();
hashSet2.add(new BigDecimal("1.0").stripTrailingZeros());
System.out.println(hashSet2.contains(new BigDecimal("1.000").stripTrailingZeros()));//返回true
2
3