在JAVA和JS浮点型数值直接计算中,经常会出现一些精度丢失的情况。
JAVA和JS采用的是IEEE 754规范,存储的位数都是固定的。而一些十进制数值在转换为二进制的时候会出现无限循环的情况,这就导致有限的存储位置会只保留无限数值中的部分,从而使得数据出现精度丢失的问题。
比如 0.1表示为二进制
0.12 = 0.2 0
0.22 = 0.4 0
0.42 = 0.8 0
0.82 = 1.6 1
0.62 = 1.2 1
0.22 = 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
}
}
网友评论