前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家java】Java数值运算 [加减乘除] 精度丢失原因分析,提供保证精度的MathHelper工具类

【小家java】Java数值运算 [加减乘除] 精度丢失原因分析,提供保证精度的MathHelper工具类

作者头像
YourBatman
发布2019-09-03 15:18:25
1.7K0
发布2019-09-03 15:18:25
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

啥都不说,先看一个例子

代码语言:javascript
复制
    public static void main(String[] args) {
        //加减乘除都出现了对应的精度问题
        System.out.println(0.05 + 0.01); //0.060000000000000005
        System.out.println(1.0 - 0.42); //0.5800000000000001
        System.out.println(4.015 * 100); //401.49999999999994
        System.out.println(123.3 / 100); //1.2329999999999999
    }

有没有一种触目惊心的感觉,感觉回去检查检查自己的代码,有没有一些数值运算吧,哈哈。这个问题相当严重,比如你有9.999999999999元,你的计算机是不会认为你可以购买10元的商品的。在有的编程语言中提供了专门的货币类型来处理这种情况,但是Java没有。

下面会解释原因以及提出解决方案。但结论可以先给大家:

Java中的简单浮点数类型float和double不能够进行运算。

问题分析

我们的第一个反应是做四舍五入。Math类中的round方法不能设置保留几位小数,我们只能象这样(保留两位):

代码语言:javascript
复制
public double round(double value){
    return Math.round(value*100)/100.0;
}

非常不幸,上面的代码并不能正常工作,给这个方法传入4.015它将返回4.01而不是4.02,如我们在上面看到的4.015*100=401.49999999999994

因此如果我们要做到精确的四舍五入,不能利用简单类型做任何运算 java.text.DecimalFormat也不能解决这个问题:

代码语言:javascript
复制
System.out.println(new java.text.DecimalFormat("0.00").format(4.025)); //4.02

现在我们已经可以解决这个问题了,原则是使用BigDecimal并且一定要用String来构造。否则见下面例子

代码语言:javascript
复制
    public static void main(String[] args) {
        double g = 12.35;
        BigDecimal bigG = new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP);
        System.out.println(bigG.doubleValue()); //期望得到12.4  但实际输出:12.3
    }

大概原因:定义double g= 12.35; 而在计算机中二进制表示可能这是样:定义了一个g=12.34444444444444449, new BigDecimal(g) g还是12.34444444444444449 new BigDecimal(g).setScale(1, BigDecimal.ROUND_HALF_UP); 得到12.3

正确的定义方式是使用字符串构造函数:

代码语言:javascript
复制
new BigDecimal("12.35").setScale(1, BigDecimal.ROUND_HALF_UP)
解决方案

上面也已经提到过了,我们可以借助BigDecimal来解决这个问题。因此此处我提供一共工具类,**以后大家java中的数值运算都采用此工具类处理,就绝对不会有精度问题了:MathHelper **

代码语言:javascript
复制
import java.math.BigDecimal;

/**
 * 精确的加减乘除的工具类
 *
 * @author fangshixiang
 * @description //
 * @date 2019/1/16 16:34
 */
public abstract class MathHelper {

    //默认10才能进位
    private static final int DEF_DIV_SCALE = 10;

    /**
     * 提供精确的加法运算。
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和 double
     */
    public static BigDecimal add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2);
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差 double
     */
    public static BigDecimal sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2);
    }

    /**
     * 提供精确的乘法运算。
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积 double
     */
    public static BigDecimal mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2);
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到 小数点以后10位,以后的数字四舍五入。
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商 double
     */
    public static BigDecimal div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指 定精度,以后的数字四舍五入。
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商 double
     */
    public static BigDecimal 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, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 提供精确的小数位四舍五入处理。
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果 double
     */
    public static BigDecimal 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));
        BigDecimal one = new BigDecimal("1");
        return b.divide(one, scale, BigDecimal.ROUND_HALF_UP);
    }
}

提供加减乘除、四舍五入的方法。保证精度。返回值类型为保证精度的BigDecimal类型,根据业务需要请转换为自己需要的类型。

下面我们利用此工具类把上面例子再跑一次:

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(add(0.05, 0.01)); //0.06
        System.out.println(sub(1.0, 0.42)); //0.58
        System.out.println(mul(4.015, 100)); //401.5000
        System.out.println(div(123.3, 100)); //1.2330000000

        //返回值用double展示  可以保证精度
        System.out.println(add(0.05, 0.01).doubleValue()); //0.06
        System.out.println(sub(1.0, 0.42).doubleValue()); //0.58
        System.out.println(mul(4.015, 100).doubleValue()); //401.5
        System.out.println(div(123.3, 100).doubleValue()); //1.233
    }
代码语言:javascript
复制
    public static void main(String[] args) {
        Integer i = 1;
        Long l = 1L;
        Float f = 1.0f;

        //这样转换成double类型 或者直接强转
        System.out.println(i.doubleValue()); //1.0
        System.out.println(l.doubleValue()); //1.0
        System.out.println(f.doubleValue()); //1.0
    }

如上,若返回值不知道是整数还是小数,请使用double接收。因为double强转为int是会失去精度的

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(add(0.05, 0.01).intValue()); //0
        System.out.println(sub(1.0, 0.42).intValue()); //0
        System.out.println(mul(4.015, 100).intValue()); //401
        System.out.println(div(123.3, 100).intValue()); //1

        //double强转为int、long问题   它会直接舍弃小数部分保留整数部分  需要注意
        System.out.println((int) 3.41); //3
        System.out.println((int) 3.81); //3
    }
失掉精度的根本原因解释

计算机进行的是二进制运算,我们输入的十进制数字会先转换成二进制,进行运算后再转换为十进制输出。Float和Double提供了快速的运算,然而问题在于转换为二进制的时候,有些数字不能完全转换,只能无限接近于原本的值,这就导致了在后来的运算会出现不正确结果的情况。

浮点数由两部分组成:指数和尾数,这点如果知道怎样进行浮点数的二进制与十进制转换,应该是不难理解的。

如果在这个转换的过程中,浮点数参与了计算,那么转换的过程就会变得不可预 知,并且变得不可逆。

我们有理由相信,就是在这个过程中,发生了精度的丢失。而至于为什么有些浮点计算会得到准确的结果,应该也是碰巧那个计算的二进制与 十进制之间能够准确转换。而当输出单个浮点型数据的时候,可以正确输出

代码语言:javascript
复制
double d = 2.4;
System.out.println(d); //2.4而不是2.3999999999999999

也就是说,不进行浮点计算的时候,在十进制里浮点数能正确显示。事实上,浮点数并不适合用于精确计算,而适合进行科学计算。

十进制小数的二进制表示:

整数部分:除以2,取出余数,商继续除以2,直到得到0为止,将取出的余数逆序 小数部分:乘以2,然后取出整数部分,将剩下的小数部分继续乘以2,然后再取整数部分,一直取到小数部分为零为止。如果永远不为零,则按要求保留足够位数的小数,最后一位做0舍1入。将取出的整数顺序排列。(因此肯定就可能失精度了)

小知识点

既然float和double型用来表示带有小数点的数,那为什么我们不称 它们为“小数”或者“实数”,要叫浮点数呢?因为这些数都以科学计数法的形式存储。当一个数如50.534,转换成科学计数法的形式为5.053e1,它 的小数点移动到了一个新的位置(即浮动了)。可见,浮点数本来就是用于科学计算的,用来进行精确计算实在太不合适了。

举个栗子

整数除以2肯定会有个尽头的,之后二进制还原成十进制只需要乘以2即可。所以只有浮点数才可能存在精度问题

代码语言:javascript
复制
    public static void main(String[] args) {
        System.out.println(2.0 -1.1); //0.8999999999999999
    }

double类型是8个字节,有效位15位。其中52小数,11位偏指数,1位符号。其中1.1是没有办法用二进制精确表示的。 1.1的二进制大约就是这样1.0001100110011001。小数部分一直是1001所以,只能取一个52精度的数近似代替1.1.因此,最终结果肯定会有误差。

同理,任意一个整数都是可以使用二进制精确表示,所以只要不超过精度总可以精确表示,但是小数往往不能使用二进制精确表示

JDK提供的Math类

Math类为Java类库提供给我们的处理一些数学运算的。由于很多读者其实不是很熟,因此本人整理如下一个Demo出来,仅供参考哈

代码语言:javascript
复制
    public static void main(String[] args) {
       int divive = 2;

       double i = 1 / divive;
       int ii = 1 / divive;
       double iii = 1.0 / divive;
       System.out.println(i); //0.0
       System.out.println(ii); //0
       System.out.println(iii); //0.5

       System.out.println(Math.ceil(1.01)); //2.0
       System.out.println(Math.ceil(0.5)); //1.0

       //三角函数方法
       System.out.println("90 度的正弦值:" + Math.sin(Math.PI / 2)); //90 度的正弦值:1.0
       System.out.println("0 度的余弦值:" + Math.cos(0)); //0 度的余弦值:1.0
       System.out.println("60 度的正切值:" + Math.tan(Math.PI / 3)); //60 度的正切值:1.7320508075688767
       System.out.println("2 的平方根与 2 商的反正弦值:" + Math.asin(Math.sqrt(2) / 2)); //2 的平方根与 2 商的反正弦值:0.7853981633974484
       System.out.println("2 的平方根与 2 商的反余弦值:" + Math.acos(Math.sqrt(2) / 2)); //2 的平方根与 2 商的反余弦值:0.7853981633974483
       System.out.println("1 的反正切值:" + Math.atan(1)); //1 的反正切值:0.7853981633974483
       System.out.println("120 度的弧度值:" + Math.toRadians(120.0)); //120 度的弧度值:2.0943951023931953
       System.out.println("π/2 的角度值:" + Math.toDegrees(Math.PI / 2)); //π/2 的角度值:90.0

       //指数函数方法
       System.out.println("e 的平方值:" + Math.exp(2)); //e 的平方值:7.38905609893065
       System.out.println("以 e 为底 2  的对数值:" + Math.log(2)); //以 e 为底 2  的对数值:0.6931471805599453
       System.out.println("以 10 为底 2  的对数值:" + Math.log10(2)); //以 10 为底 2  的对数值:0.3010299956639812
       System.out.println("4 的平方根值:" + Math.sqrt(2)); //4 的平方根值:1.4142135623730951
       System.out.println("8 的立平方根值:" + Math.cbrt(2)); //8 的立平方根值:1.2599210498948732
       System.out.println("2 的 2 次方值:" + Math.pow(2, 2)); //2 的 2 次方值:4.0

       //取整函数方法
       System.out.println("使用 ceil() 方法取整:" + Math.ceil(5.2)); //向上取整:6.0
       System.out.println("使用 floor() 方法取整:" + Math.floor(2.5)); //向下取整:2.0

       //rint():返回最接近参数的整数,如果有2个数同样接近,则返回偶数的那个。
       //round()就是数学上的四舍五入。
       System.out.println("使用 rint() 方法取整:" + Math.rint(2.7)); // 四舍五入:3.0(此方法非常特殊)
       System.out.println("使用 int round() 方法取整:" + Math.round(3.4f)); //四舍五入:3
       System.out.println("使用 long round() 方法取整:" + Math.round(2.5)); //四舍五入:3

       //最大、最小值
       System.out.println("4 和 8 较大者:" + Math.max(4, 8)); //8
       System.out.println("4.4 和 4 较小者:" + Math.min(4.4, 4)); //4.0
       System.out.println("-7 的绝对值:" + Math.abs(-7)); //7

   }
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年01月17日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题分析
  • 解决方案
  • 失掉精度的根本原因解释
    • 十进制小数的二进制表示:
      • 小知识点
        • 举个栗子
        • JDK提供的Math类
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档