数值计算应用(BigDecimal应用、浮点数计算、数值溢出)

浮点数运算精度丢失

浮点数 float 或 double 运算时的精度丢失

浮点数 float 或 double 运算时的精度问题场景

先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算:

public class DoubleAndFloatPrecision {
    public static void main(String[] args) {
        System.out.println(0.1+0.2);
        System.out.println(1.0-0.8);
        System.out.println(5.015*100);
        System.out.println(123.3/100);

        double doubleExample1 = 2.15;
        double doubleExample2 = 1.10;
        boolean doubleResult = doubleExample1 - doubleExample2 == 1.05;
        System.out.println("double类型计算结果精度是否准确:  " + doubleResult);
        System.out.println("double类型计算结果为:  " + (doubleExample1 - doubleExample2));

        float floatExample1 = 2.15f;
        float floatExample2 = 1.10f;
        boolean floatResult = floatExample1 - floatExample2 == 1.05;
        System.out.println("float类型计算结果为:  " + (floatExample1 - floatExample2));
        System.out.println("float类型计算结果精度是否准确:  " + floatResult);
    }
}

输出结果如下:

0.30000000000000004
0.19999999999999996
501.49999999999994
1.2329999999999999
double类型计算结果精度是否准确:  false
double类型计算结果为:  1.0499999999999998
float类型计算结果为:  1.0500001
float类型计算结果精度是否准确:  false

可以看到,输出结果和我们预期的很不一样。是什么原因导致产生该问题呢

浮点数 float 或 double 运算时的精度问题产生原因

这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,浮点数也不例外。计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。

比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,类似于0.1这中 无法精确表达的数据,就是浮点数计算造成精度损失的根源。

浮点数之间的等值判断,基本数据类型不能用 == 来比较,包装数据类型不能用 equals 来判断

考虑浮点数舍入和格式化的方式

我们通过String.format 的格式化舍入方式对同为浮点数的数据进行舍入和格式化,最终得到的结果也不一定相同,可以根据下列代码分析下为什么:

        double test1 = 5.35;
        float test2 = 5.35f;
        //四舍五入
        System.out.println("double类型的5.35四舍五入数据为: " + String.format("%.1f", test1));
        System.out.println("float类型的5.35四舍五入数据为: " + String.format("%.1f", test2));

代码执行结果如下:

double类型的5.35四舍五入数据为: 5.4
float类型的5.35四舍五入数据为: 5.3

产生该问题的原因就是由精度问题和舍入方式共同导致的,double 和 float 的 5.35 其实相当于:

5.350000000000000088817841970012523233890533447265625
5.349999904632568359375

String.format 采用四舍五入的方式进行舍入,取 1 位小数,double 的 3.350 四舍五入为 3.4,而 float 的 3.349 四舍五入为 3.3。

BigDecimal 的应用

BigDecimal应用注意事项

使用 BigDecimal 表示和计算浮点数,务必使用字符串的构造方法来初始化 BigDecimal

当我们代码中接收外部系统的数据为浮点数时,我们直接将浮点数数据转为BigDecimal,代码如下:

public class BigDecimalPrecision {
    public static void main(String[] args) {
        System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
        System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.2)));
        System.out.println(new BigDecimal(5.015).multiply(new BigDecimal(100)));
        System.out.println(new BigDecimal(123.4).divide(new BigDecimal(100)));
    }
}

代码执行结果如下:

0.3000000000000000166533453693773481063544750213623046875
0.799999999999999988897769753748434595763683319091796875
501.49999999999996802557689079549163579940795898437500
1.23400000000000005684341886080801486968994140625

根据代码执行结果可以分析出运算结果还是不精确,只不过是精度变高;
根据代码结果分析得出BigDecimal使用的第一个注意事项:使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal
查看下改进的代码

public class BigDecimalPrecisionOptimize {
    public static void main(String[] args) {
        System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
        System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.2")));
        System.out.println(new BigDecimal("5.015").multiply(new BigDecimal("100")));
        System.out.println(new BigDecimal("123.4").divide(new BigDecimal("100")));
    }
}

代码结果如下:

0.3
0.8
501.500
1.234

浮点数的字符串格式化也要通过 BigDecimal 进行

举例:一个Double类型数据,通过使用本身的Double.toString()方法,转化成BigDecimal类型数据精度是否存在问题?

        System.out.println(new BigDecimal("5.015").multiply(new BigDecimal(Double.toString(100))));

代码执行结果如下:

501.5000

根据代码执行结果与上文中得出的结果精度不一致,这是因为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 相加。所以,初始化 100 的两种不同方式,导致最后结果的 scale 分别是 4 和 3。最终得出的结果精度错误。

举例:BigDecimal 来格式化数字 分别使用向下舍入和四舍五入方式取 1 位小数进行格式化:

BigDecimal test1 = num1.setScale(1, BigDecimal.ROUND_DOWN);
BigDecimal test2 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);

BigDecimal的等值比较问题

BigDecimal的等值比较时,如果我们希望只比较 BigDecimal 的 value,不确定数据精度时,不要使用equals()方法,应使用compareTo() 方法,因为equals() 方法不仅仅会比较值的大小(value)还会比较精度(scale),而 compareTo() 方法比较的时候会忽略精度。

两个BigDecimal类型数据大小的比较

compareTo() : 返回 -1 表示 a 小于 b,0 表示 a 等于 b , 1 表示 a 大于 b
代码如下:

 BigDecimal bigDecimalTest1 = new BigDecimal("0.2") ;
        BigDecimal bigDecimalTest2 = new BigDecimal("0.3") ;
        BigDecimal bigDecimalTest3 = new BigDecimal("0.3") ;
        System.out.println("bigDecimalTest1是否比bigDecimalTest2大:" + bigDecimalTest1.compareTo(bigDecimalTest2));
        System.out.println("bigDecimalTest1是否比bigDecimalTest2小:" + bigDecimalTest2.compareTo(bigDecimalTest1));
        System.out.println("bigDecimalTest3是否与bigDecimalTest2相等:" + bigDecimalTest3.compareTo(bigDecimalTest2));

代码执行结果如下:

bigDecimalTest1是否比bigDecimalTest2大:-1
bigDecimalTest1是否比bigDecimalTest2小:1
bigDecimalTest3是否与bigDecimalTest2相等:0

BigDecimal转为String

数值溢出问题

基本类型的内存溢出

举例:对 long 的最大值进行 +1 操作,会发现最终结果展示为一个负数,long类型数据的最小值。显然这是发生了溢出,而且是默默地溢出,并没有任何异常。这种情况有两种解决方案:

考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法

Math 类的xxExact 方法进行数值运算时,这些方法可以在数值溢出时主动抛出异常。

使用BigInteger和BigDecimal。

BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值