这个问题挺复杂的,以前看过一次,但没太看懂,这次整理了一下。有兴趣的同学可以看看。
首先打印出0.14和0.14*100的二进制(程序见参考):
0.14 :      0 01111111100 0001111010111000010100011110101110000101000111101100
0.14*100 :  0 10000000010 1100000000000000000000000000000000000000000000000001
14.0 :      0 10000000010 1100000000000000000000000000000000000000000000000000
我们可以看到0.14的小数部分出现循环,就像10/3 = 1.33333…一样,所以0.14是有精度丢失的,
比较0.14*100和14.0,0.14*100多了最后一位1。Java遵循IEEE 754标准来支持浮点数的操作,结合浮点值0.14,我们来看一下,1) 0.14的二进制格式
0 01111111100 0001111010111000010100011110101110000101000111101100.
根据IEEE 754标准,
 
注:S:符号(0正1负),E,指数,M小数部分。
对0.14格式化后的结果是:
S(1位) E(11位) M (52位)
0 01111111100 0001111010111000010100011110101110000101000111101100
根据计算公式:
 
我们可以得到e = 01111111100 – 1023 = 1020 – 1023 = -3
m = 1 (隐藏位) + 0.0001111010111000010100011110101110000101000111101100 = 1 + 
Long.valueOf("0001111010111000010100011110101110000101000111101100", 2)/ (Math.pow(2, 52) – 1)
= 1.12000000000000013
n = 1.12000000000000013 *2^-3= 1.12000000000000013/8  约 0.14
接下来,第二个问题,2)为什么0.14 * 100 输出14.000000000000002?
由上可知0.14是不精确的,乘100只会扩大这个不精确度。具体的计算过程如下:
100的浮点二进制:
0 10000000101 1001000000000000000000000000000000000000000000000000
跟据浮点乘法规则指数相加,小数相乘,得到
0.14 * 100 = 
2^(6-3) //100的指数是6,0.14的指数是-3

(1. 1001000000000000000000000000000000000000000000000000*
1. 0001111010111000010100011110101110000101000111101100) //小数部分
= 2^(6-3) * (1 + 0.1001000000000000000000000000000000000000000000000000 + 
0.0001111010111000010100011110101110000101000111101100 + 
0. 1001000000000000000000000000000000000000000000000000 * 0. 0001111010111000010100011110101110000101000111101100
//方便计算,分解乘数(同1.1 * 1.1 = 1 + 0.1 * 1 + 1 * 0.1 + 0.1*0.1)
这部分我用计算器计算,貌似精度丢失更严重,但可以得知小数部分依然是循环,而不能精确表达。同时,通过查看JDK中Double.toString(d)方法,我们可以看到SUN实现的浮点输出方法。所以最后打印在页面的是14.000000000000002而不是14.0.故,浮点是用有限二进位接近表达一个数值,不精确是常态,使用要慎重参考(强烈推荐)
IEEE 754 浮点数的表示精度探讨
http://www.cnblogs.com/bossin/archive/2007/04/08/704567.html
解读IEEE标准754:浮点数表示
http://www.linuxsir.org/bbs/thread262207.html
浮点乘法计算
http://www.cs.umd.edu/class/spring2003/cmsc311/Notes/BinMath/multFloat.html
打印DOUBLE二进制方法:
public class DoubleTest {
@Test
public void test(){
System.out.println("0.5 : " + this.dumpDouble(0.5));
System.out.println("0.14 : " + this.dumpDouble(0.14));
System.out.println("0.14*100 : " + this.dumpDouble(0.14 * 100));
System.out.println("14.0 : " + this.dumpDouble(14.0));
System.out.println("100 : " + this.dumpDouble(100));

Assert.assertEquals("100.0%", getPercentage(0.9999, 2));
Assert.assertEquals("100%", getPercentage(0.9999, 1));
Assert.assertEquals("99.9%", getPercentage(0.999, 2));
Assert.assertEquals("10.1%", getPercentage(0.101, 2));
Assert.assertEquals("10.2%", getPercentage(0.1015, 2));
Assert.assertEquals("11.0%", getPercentage(0.1095, 2));
Assert.assertEquals("0.11%", getPercentage(0.0011, 3));

final int TOTAL = 100000;
double[] data = new double[TOTAL];

for(int i = 0; i < TOTAL; i++ ){
data[i] = Math.random();
}

//通过FORMAT,获得百分比,忽略精度
DecimalFormat f = new DecimalFormat("0.##'.0'%");
long start = System.currentTimeMillis();
for(int i = 0; i < TOTAL; i++ ){
f.format(data[i]);
}
long end = System.currentTimeMillis();
System.out.println("0 time: " + (end - start));

//基于字符计算百分比
start = System.currentTimeMillis();
for(int i = 0; i < TOTAL; i++ ){
this.getPercentage(data[i], 2);
}
end = System.currentTimeMillis();
System.out.println("1 time: " + (end - start));
} public String dumpDouble(double d) {
StringBuilder b = new StringBuilder();
String bString = Long.toBinaryString(Double.doubleToLongBits(d));

int fillZeros = 64 - bString.length();
for(int i = 0; i < fillZeros ; i++){
if(i == 1){
b.append(' ');
}
b.append('0');
}

for(int i = 0; i < bString.length(); i++ ){
if((i + fillZeros) == 1 || (i + fillZeros) == 12){
b.append(' ');
}
b.append(bString.charAt(i));
}

return b.toString();
}

char[] next = {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0',};
public String getPercentage(double d, int precision){
StringBuilder b = new StringBuilder(Double.toString(d * 100));
int dot = b.indexOf(".");
if(dot > 0 && dot + precision < b.length()){
char c = b.charAt(dot + precision);
//四舍五入
if(c > '4'){
int j = dot + precision - 1;
if(j == dot) {
//跳过小数点
j --;
}
char n = next[b.charAt(j) - '0'];
b.setCharAt(j, n);
//向前进1
while(n == '0' && j > 0){
j --;
if(j == dot) {
//跳过小数点
j --;
}
//+1
n = next[b.charAt(j) - '0'];
b.setCharAt(j, n);

}
if(j == 0){
b.insert(0, 1);
dot++;
if(precision == 1){
b.setCharAt(dot, '%');
return b.substring(0, dot + 1);
}else{
b.setCharAt(dot + precision, '%');
return b.substring(0, dot + precision + 1);
}
}
}
b.setCharAt(dot + precision, '%');
return b.substring(0, dot + precision + 1);
}
b.append("%");
return b.toString();
}
}

解决方案 »

  1.   

    以前看过类似的,是因为计算机存储的缺陷导致的~~
    所以一般要求极精确的用BigDecimal~~~
      

  2.   

    楼主有空看看
    《Java 解惑》
    就明白了
      

  3.   

    和存储有关,但不是缺限,这么牛的算法,IEEE 754
    至于BigDecimal有点慢吧,呵呵
      

  4.   


    //昨天看了一道面试题,迷惑不解 float number1 = (float)(123/0);
    // 会抛异常java.lang.ArithmeticException

    float number2 = (float)(123/0.0);
    //不会抛异常
      

  5.   

    除0.0得到无限大,可以参考下引用的链接。
    另外,计算百分比的算法,我现在想到的三种,以保留小数点一位为例,
    1) DecimalFormat, 
    DecimalFormat f = new DecimalFormat("##0.#%");2) BigDecimal, 
    BigDecimal b = new BigDecimal(data[i] * 100);
    b.round(new MathContext(3));3) 基于字符计算
    见参考的getPercentage()方法。随机计算10万次所用时间,
    DecimalFormat: 280
    BigDecimal:497
    Character: 193
      

  6.   

    二进制系统不能精确的表示1/10,就像十进制不能精确表示1/3一样,浮点计算会丢失精度,精确计算用BigDecimal吧
      

  7.   

    最近做模拟在计算数据的时候也总是会遇到这样的问题
    最好将这个double或者float数据进行bigdecimal处理
      

  8.   

    从根上将是浮点数在计算机中表示造成的,你可以查一下double是怎样在计算机中存储的。然后自己动手算一下0.1换算成二进制表示会是怎么样的情况,你就能知道了。你System.out.println(someDoubleValue)的时候
    这个方法会自动做处理的,所以你打印出来的double类型的数跟你定义的是一样的,但是又计算后本来在存储精度损失的数据再以计算那么System.out.println()这个方法也搞不定了。so......