Java中数据处理精度丢失的问题
关于Java中数据处理精度丢失的问题
数值之间的转换
Java中经常需要将⼀种数值类型转换为另外⼀种数值类型,下图就给出了数值类型之间的合法转换。
bigdecimal除法保留小数()
实线代表合法转换即⽆信息丢失的转换,虚线表⽰转换可能存在精度丢失问题。
在进⾏两个数值的运算时,
如果两个操作数中有⼀个是double类型的,另外⼀个⾃动转换为double类型。
如果其中⼀个操作数是float类型的,另外⼀个操作数也将⾃动转换为float类型。
如果其中⼀个操作数是long类型的,另外⼀个操作数也将⾃动转换为long类型。
否则,两个操作数都会被转换为int类型。
所以在对两个不同类型操作数进⾏运算时,就可能存在精度丢失的问题。
例如:当⼀个int型数值和⼀个float型数值进⾏运算时,int型操作数将会⾃动转换为float型操作数。那么int型数值是如何转换为float型数值的?⾸先要了解他们是如何存储在计算机中的。整型在计算机中以第⼀位表⽰符号,剩余尾数表⽰数值的⼀串⼆级制数字来表⽰,⽽浮点型⽆论单精度还是双精度都有相同的存储⽅式:
符号位
指数位
尾数位
其中符号位表⽰浮点数的正负,指数位表⽰其的阶位,以移位的形势表现,尾数为表⽰其数值。其中float 1位符号位,8位指数位,23位尾数位。double 1位符号位,11位指数位,52位尾数位。
所以当⼀个int 类型的数值转换为float类型是,int型数值的32位拆分为 1位符号位,8位指数位,23位尾数位,改变了原本的表⽰⽅式,故产⽣了精度丢失。
其他的转换类似,当位数不够时通过补0来凑够位数。
java中double和float精度丢失问题
这是java和其它计算机语⾔都会出现的问题,下⾯我们分析⼀下为什么会出现这个问题:
float和double类型主要是为了科学计算和⼯程计算⽽设计的。他们执⾏⼆进制浮点运算,这是为了在⼴泛的数字范围上提供较为精确的快速近似计算⽽精⼼设计的。然⽽,它们并没有提供完全精确的结
果,所以我们不应该⽤于精确计算的场合。float和double类型尤其不适合⽤于货币运算,因为要让⼀个float或double精确的表⽰0.1或者10的任何其他负数次⽅值是不可能的(其实道理很简单,⼗进制系统中能不能准确表⽰出1/3呢?同样⼆进制系统也⽆法准确表⽰1/10)。
浮点运算很少是精确的,只要是超过精度能表⽰的范围就会产⽣误差。往往产⽣误差不是因为数的⼤⼩,⽽是因为数的精度。因此,产⽣的结果接近但不等于想要的结果。尤其在使⽤ float 和 double 作精确运算的时候要特别⼩⼼。
现在我们就详细剖析⼀下浮点型运算为什么会造成精度丢失?
⾸先我们要搞清楚下⾯两个问题:
(1)⼗进制整数如何转化为⼆进制数
算法很简单。举个例⼦,11表⽰成⼆进制数:
11/2=5余1
5/2=2余1
2/2=1余0
1/2=0余1
0结束⼆进制表⽰为(从下往上):1011
这⾥提⼀点:只要遇到除以后的结果为0了就结束了,⼤家想⼀想,所有的整数除以2是不是⼀定能够最终得到0。换句话说,所有的整数转变为⼆进制数的算法会不会⽆限循环下去呢?绝对不会,整数永远可以⽤⼆进制精确表⽰,但⼩数就不⼀定了。
(2)⼗进制⼩数如何转化为⼆进制数
算法是乘以2直到没有了⼩数为⽌。举个例⼦,0.9表⽰成⼆进制数
0.9*2=1.8取整数部分1
0.8(1.8的⼩数部分)*2=1.6取整数部分1
0.6*2=1.2取整数部分1
0.2*2=0.4取整数部分0
0.4*2=0.8取整数部分0
0.8*2=1.6取整数部分1
0.6*2=1.2取整数部分0
.........0.9⼆进制表⽰为(从上往下):
注意:上⾯的计算过程循环了,也就是说*2永远不可能消灭⼩数部分,这样算法将⽆限下去。很显然,⼩数的⼆进制表⽰有时是不可能精确的。其实道理很简单,⼗进制系统中能不能准确表⽰出1/3呢?同样⼆进制系统也⽆法准确表⽰1/10。这也就解释了为什么浮点型减法出现了"减不尽"的精度丢失问题。
解决⽅法
使⽤《Effective Java》这本书中的BigDecmal,⽽且需要在构造参数使⽤String类型。
BigDecimal类有4个构造⽅法,但这⾥只关⼼对解决浮点型数据进⾏精确计算有⽤的⽅法,即
BigDecimal(double value) // 将double型数据转换成BigDecimal型数据
思路:先通过BigDecimal(double value)⽅法,将double型数据转换成BigDecimal数据,然后再正常进
⾏精确计算了。计算完毕后,对结果做⼀些处理,⽐如:对除不尽的结果可以进⾏四舍五⼊。最后,再把结果由BigDecimal型数据转换回double型数据。
但是根据BigDecimal的详细说明,如果需要精确计算,不能直接⽤double,要⽤ String来构造BigDecimal。所以使⽤BigDecimal类的另⼀个⽅法,即能够帮助我们正确完成精确计算的 BigDecimal(String value)⽅法。
// BigDecimal(String value)能够将String型数据转换成BigDecimal型数据
那么问题来了:如果我们要做⼀个浮点型数据的加法运算,需要先将两个浮点数转为String型数据,然后⽤ BigDecimal(String value)构造成BigDecimal,之后要在其中⼀个上调⽤add⽅法,传⼊另⼀个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。如果每次做浮点型数据的计算都要如此,这个过程将⾮常繁琐,所以相对好的办法,就是写⼀个类,在类中完成这些繁琐的转换过程。这样在需要进⾏浮点型数据计算的时候,只要调⽤这个类就可以了。⽹上已经有⼤佬提供了⼀个⼯具类Arith来完成这些转换操作。它提供以下静态⽅法,可以完成浮点型数据的加减乘除运算和对其结果进⾏四舍五⼊的操作:
package com.util;
import java.math.BigDecimal;
/**
* 由于Java的简单类型不能够精确的对浮点数进⾏运算,这个⼯具类提供精确的浮点数运算,包括加減乘除和四捨五⼊。
*/
public class Arith {
//默认吃吃饭运算精度
private static final int DEF_DIV_SCALE =10;
//这个类不能实例化
private Arith(){
}
/**
* 提供精确的加法运算
*
* @param v1
*            被加数
* @param v2
*            加数
* @return 两个参数的和
*/
public static double add(double v1,double v2){
BigDecimal b1 =new String(v1));
BigDecimal b2 =new String(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算
* @param v1
*            被減数
* @param v2
*            減数
* @return两个参数的差
*/
public static double sub(double v1,double v2){
BigDecimal b1 =new String(v1));
BigDecimal b2 =new String(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算
*
* @param v1
*            被乘数
* @param v2
*            乘数
* @return 两个参数的积
*/
public static double mul(double v1,double v2){
BigDecimal b1 =new String(v1));
BigDecimal b2 =new String(v2));
return b1.multiply(b2).doubleValue();
return b1.multiply(b2).doubleValue();
}
/**
* 提供(相对)精确的除⾮运算,当发⽣除不尽的情况时,精确到⼩数点以后10位,以后的数字四舍五⼊
* @param v1
*            被除數
* @param v2
*            除數
* @return 兩個參數的商
*/
public static double div(double v1,double v2){
return div(v1, v2, DEF_DIV_SCALE);
}
/**
* 提供(相对)精确的除法运算。当发⽣除不尽的情况时,由scale参数指定精度,以后的数字四舍五⼊
* @param v1
*            被除數
* @param v2
*            除數
* @param scale
*            表⽰表⽰需要精確到⼩數點以後位数。
* @return 兩個參數的商
*/
public static double 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 String(v1));
BigDecimal b2 =new String(v2));
return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
/**
* 提供精確的⼩數位四捨五⼊處理。
* 提供精确的⼩数位四舍五⼊处理
*
* @param v
*            需要四捨五⼊的數位
* @param scale
*            ⼩數點後保留幾位
* @return 四捨五⼊後的結果
*/
public static double round(double v,int scale){
if(scale <0){
throw new IllegalArgumentException(
"The scale must be a positive integer or zero");
}
BigDecimal b =new String(v));
BigDecimal one =new BigDecimal("1");
return b.divide(one, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
附上Arith的源代码,只要把它编译保存好,要进⾏浮点数计算的时候,在你的源程序中导⼊Arith类就可以使⽤以上静态⽅法来进⾏浮点数的精确计算了。
参考资料:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。