美文网首页
JAVA/JS 精度丢失问题

JAVA/JS 精度丢失问题

作者: 予_远方 | 来源:发表于2017-12-05 20:36 被阅读80次

在JAVA和JS浮点型数值直接计算中,经常会出现一些精度丢失的情况。

JAVA和JS采用的是IEEE 754规范,存储的位数都是固定的。而一些十进制数值在转换为二进制的时候会出现无限循环的情况,这就导致有限的存储位置会只保留无限数值中的部分,从而使得数据出现精度丢失的问题。

比如 0.1表示为二进制

0.12 = 0.2 0
0.2
2 = 0.4 0
0.42 = 0.8 0
0.8
2 = 1.6 1
0.62 = 1.2 1
0.2
2 = 0.4 0
......

0.0 0011 0011 0011 ...... 即会出现无限循环的数值 0011
而实际上因为字节限制,实际保留的二进制是:
0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001

同理0.2表示为二进制
0.0011 0011 0011 ... 即会进行0011的无限循环
实际保留的是:
0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011

很明显 由于精度的影响导致像0.1和0.2这样的十进制在转换为二进制的时候会出现误差,出现误差的二进制相加转换为十进制的时候结果就会出现问题了。

大整数的精度丢失本质上是和浮点数一样的,JS中能精准表示的最大整数是 Math.pow(2, 53),十进制即 9007199254740992。

只要整数不超过 Math.pow(2, 53)就不会丢失精度,对于小数的计算来说,就可以先乘倍数变成整数,再除以相同倍数。当然这个整倍的数据也不能超过Math.pow(2, 53)。

  • JS中两个小数相加减,要先计算出各变成整数需要的倍数,然后取最大的那个将两个数值都变成整数倍,然后在除以整数倍。
  • 乘法的话,先计算出各变成整数需要的倍数m,n,然后将变成整数倍的数值相乘再除以m+n的整数倍。
  • 除法的话,先计算出各变成整数需要的倍数m,n,然后将变成整数倍的数值相除再乘(除数的倍数-被除数的倍数)。

以上的其实都是通过约数来进行约分的。

JS的加减乘除相对精确计算如下:

/**
 * 加法
 * @param s1
 * @param s2
 * @returns  
 */
function floatsAdd(s1,s2){
    var m = 0 ,n = 0 , mul;
    
    try{
        m = s1.toString().split(".")[1].length;
    }catch(e){
         
    }
    
    try{
        n = s2.toString().split(".")[1].length;
    }catch(e){
        
    }
    
    mul = Math.pow(10,Math.max(m,n));
    
    return (s1*mul+s2*mul)/mul;
}

/**
 * 减法
 * @param s1 被减数
 * @param s2 减数
 * @returns  
 */
function floatsSub(s1,s2){
    var m =0 , n = 0 , mul;
    
    try{
        m = s1.toString().split(".")[1].length;
    }catch(e){
         
    }
    
    try{
        n = s2.toString().split(".")[1].length;
    }catch(e){
        
    }
    
    mul = Math.pow(10,Math.max(m,n));
    
    return (s1*mul-s2*mul)/mul;
}

/**
 * 乘法
 * @param s1 
 * @param s2 
 * @returns  
 */
function floatsMul(s1,s2){
     var m =0 ;
     s1 = s1.toString();
     s2 = s2.toString();
     
     try{
         m+= s1.split(".")[1].length;
     }catch(e){}
     
     try{
        m+= s2.split(".")[1].length;
     }catch(e){}
    
     return (Number(s1.replace(".",""))*Number(s2.replace(".","")))/Math.pow(10,m)
}

/**
 * 除法
 * @param s1 被除数
 * @param s2 除数
 * @returns  
 */
function floatsDiv(s1,s2){
    var m=0,n=0,r1,r2;
    s1 = s1.toString();
    s2 = s2.toString();
    try{
        m = s1.split(".")[1].length;
    }catch(e){}
    
    try{
        n = s2.split(".")[1].length;
    }catch(e){}
    
    r1 = Number(s1.replace(".",""));
    r2 = Number(s2.replace(".",""));
    
    return (r1/r2)*Math.pow(10,n-m);
}

简单测试例子:

    var arg1 =0.2,arg2=0.1;
    console.log(arg1+"+"+arg2+" 直接相加: "+(arg1+arg2)); //0.2+0.1 直接相加: 0.30000000000000004
    console.log(arg1+"-"+arg2+" 直接相减: "+(arg1-arg2)); //0.2-0.1 直接相减: 0.1
    console.log(arg1+"*"+arg2+" 直接相乘: "+(arg1*arg2)); //0.2*0.1 直接相乘: 0.020000000000000004
    console.log(arg1+"/"+arg2+" 直接相除: "+(arg1/arg2)); //0.2/0.1 直接相除: 2
    console.log(arg1+"+"+arg2+" 倍数相加: "+floatsAdd(arg1,arg2)); //0.2+0.1 倍数相加: 0.3
    console.log(arg1+"-"+arg2+" 倍数相减: "+floatsSub(arg1,arg2)); //0.2-0.1 倍数相减: 0.1
    console.log(arg1+"*"+arg2+" 倍数相乘: "+floatsMul(arg1,arg2)); //0.2*0.1 倍数相乘: 0.02
    console.log(arg1+"/"+arg2+" 倍数相除: "+floatsDiv(arg1,arg2)); //0.2/0.1 倍数相除: 2

在JAVA中可以通过BigDecimal来进行计算,在使用BigDecimal的时候要注意

  • 尽量用String 来初始化
  • 比较的话用compareTo

至于原因可以通过下面的例子来说明

package com.ren.util;

import java.math.BigDecimal;
import java.text.DecimalFormat;

public class BigDecimalUtil {
    
    /**
     * 
    * @Title: add 
    * @Description: 加法
    * @param @param s1 
    * @param @param s2
    * @return double    返回类型 
    * @throws
     */
    public static double add(String s1,String s2){
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal(s2);
        return b1.add(b2).doubleValue();
    }
    
    /**
     * 
    * @Title: sub 
    * @Description: 减法
    * @param @param s1 被减数
    * @param @param s2 减数
    * @return double    返回类型 
    * @throws
     */
    public static double sub(String s1,String s2){
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal(s2);
        return b1.subtract(b2).doubleValue();
    }
    
    /**
     * 
    * @Title: mul 
    * @Description: 乘法
    * @param @param s1 乘数
    * @param @param s2 乘数
    * @return double    返回类型 
    * @throws
     */
    public static double mul(String s1,String s2){
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal(s2);
        return b1.multiply(b2).doubleValue();
    }
    
    /**
     * 
    * @Title: div 
    * @Description: 除法 四舍五入
    * @param @param s1 被除数
    * @param @param s2 除数
    * @param @param scale 保留小数位数
    * @return double    返回类型 
    * @throws
     */
    public static double div(String s1,String s2,int scale){
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal(s2);
        return b1.divide(b2, scale,BigDecimal.ROUND_HALF_UP).doubleValue();
    }
    
    /**
     * 
    * @Title: round 
    * @Description: 四舍五入格式化保留小数位数
    * @param @param s1 原始数据
    * @param @param scale 保留的小数位数
    * @return double    返回类型 
    * @throws
     */
    public static double round(String s1,int scale){
        if(scale < 0){
            throw new RuntimeException("小说位数需要大于等于零");
        }
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal("1");
        return b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
    }
    
    /**
     * 
    * @Title: round 
    * @Description: 四舍五入格式化保留小数位数
    * @param @param s1  原始数据
    * @param @param scale 保留的小数位数
    * @param @param isFillZero 是否强制补零
    * @return String    返回类型 
    * @throws
     */
    public static String round(String s1,int scale,boolean isFillZero){
        if(scale < 0){
            throw new RuntimeException("小说位数需要大于等于零");
        }
        BigDecimal b1 = new BigDecimal(s1);
        BigDecimal b2 = new BigDecimal("1");
        double tmpValue = b1.divide(b2,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
        String returnValue =null;
        if(isFillZero){
            String param = "0";
            if(scale > 0){
                param = param +".";
                for(int i=0;i<scale;i++){
                    param = param +"0";
                }
            }
            DecimalFormat df = new DecimalFormat(param);
            returnValue = df.format(tmpValue);
        }else{
            returnValue = String.valueOf(tmpValue);
        }
        
        return returnValue;
    }

}

简单的测试例子:

package com.ren.test;

import java.math.BigDecimal;

import com.ren.util.BigDecimalUtil;

public class BigDecimalTest {
    public static void main(String[] args) {
        System.out.println("尽量用字符串来初始化=========================================");
        BigDecimal num11 = new BigDecimal(0.1); 
        System.out.println(num11);//0.1000000000000000055511151231257827021181583404541015625
        BigDecimal num22 = new BigDecimal("0.1"); 
        System.out.println(num22);//0.1
        
        System.out.println("比较大小用compareTo=========================================");
        BigDecimal num1 = new BigDecimal("0.100"); 
        System.out.println(num1);//0.100
        BigDecimal num2 = new BigDecimal("0.1"); 
        System.out.println(num2);//0.1
        System.out.println(num1 == num2);//false
        System.out.println(num1.equals(num2));//false
        System.out.println(num1.compareTo(num2)==0);//true
        
        System.out.println("小数点=========================================");
        System.out.println("ROUND_HALF_UP:向上四舍五入");
        BigDecimal b11 = new BigDecimal("1.243");
        BigDecimal b1 = new BigDecimal("1.245");
        BigDecimal b12 = new BigDecimal("1.246");
        System.out.println( b11.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_UP).doubleValue());//1.24
        System.out.println( b1.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_UP).doubleValue());//1.25
        System.out.println( b12.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_UP).doubleValue());//1.25
        System.out.println("ROUND_HALF_DOWN:向下四舍五入");
        BigDecimal b21 = new BigDecimal("1.243");
        BigDecimal b2 = new BigDecimal("1.245");
        BigDecimal b22 = new BigDecimal("1.246");
        System.out.println( b21.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_DOWN).doubleValue());//1.24
        System.out.println( b2.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_DOWN).doubleValue());//1.24
        System.out.println( b22.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_HALF_DOWN).doubleValue());//1.25
        System.out.println("ROUND_UP:直接舍弃保留后的所有小数,将最后一个保留小数进一");
        BigDecimal b31 = new BigDecimal("1.243");
        BigDecimal b3 = new BigDecimal("1.245");
        BigDecimal b32 = new BigDecimal("1.246");
        System.out.println( b31.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_UP).doubleValue());//1.25
        System.out.println( b3.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_UP).doubleValue());//1.25
        System.out.println( b32.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_UP).doubleValue());//1.25
        System.out.println("ROUND_DOWN:直接舍弃保留后的所有小数");
        BigDecimal b41 = new BigDecimal("1.243");
        BigDecimal b4 = new BigDecimal("1.245");
        BigDecimal b42 = new BigDecimal("1.246");
        System.out.println( b41.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_DOWN).doubleValue());//1.24
        System.out.println( b4.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_DOWN).doubleValue());//1.24
        System.out.println( b42.divide(new BigDecimal("1"), 2,BigDecimal.ROUND_DOWN).doubleValue());//1.24
        
        System.out.println("强制补零=========================================");
        String s1 = BigDecimalUtil.round(String.valueOf(102541.000),2,false);
        System.out.println(s1);//102541.0
        String s2 = BigDecimalUtil.round(String.valueOf(102541.000),2,true);
        System.out.println(s2);//102541.00
        System.out.println("加=========================================");
        System.out.println(BigDecimalUtil.add("0.1", "0.2"));//0.3
        System.out.println("减=========================================");
        System.out.println(BigDecimalUtil.sub("0.2", "0.1"));//0.1
        System.out.println(BigDecimalUtil.sub("0.1", "0.2"));//-0.1
        System.out.println("乘=========================================");
        System.out.println(BigDecimalUtil.mul("0.1", "0.2"));//0.02
        System.out.println("除=========================================");
        double div1 = BigDecimalUtil.div("0.2", "0.1", 2);
        System.out.println(div1);//2.0
        System.out.println(BigDecimalUtil.round(String.valueOf(div1), 2));//2.0
        System.out.println(BigDecimalUtil.round(String.valueOf(div1), 2,true));//2.00
    }
}

相关文章

网友评论

      本文标题:JAVA/JS 精度丢失问题

      本文链接:https://www.haomeiwen.com/subject/ztcovxtx.html