问题的提出:
如果我们编译运行下面这个程序会看到什么?
public static void main(String args[]){
System.out.println(0.05+0.01);
System.out.println(1.0-0.42);
System.out.println(4.015*100);
System.out.println("BigDecimal:"+new BigDecimal(Double.toString(4.015)).multiply(new BigDecimal(Double.toString(100))));
System.out.println(123.3/100);
}
你没有看错!结果确实是
0.060000000000000005
0.5800000000000001
401.49999999999994
BigDecimal:401.5000
1.2329999999999999
Java中的简单浮点数类型float和double不能够进行运算。不光是Java,在其它很多编程语言中也有这样的问题。在大多数情况下,计算的结果是准确的,但是多试几次(可以做一个循环)就可以试出类似上面的错误。现在终于理解为什么要有BCD码了。
这个问题相当严重,如果你有9.999999999999元,你的计算机是不会认为你可以购买10元的商品的。
在有的编程语言中提供了专门的货币类型来处理这种情况,但是Java没有。现在让我们看看如何解决这个问题。
四舍五入
我们的第一个反应是做四舍五入。Math类中的round方法不能设置保留几位小数,我们只能象这样(保留两位):
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也不能解决这个问题:
System.out.println(new java.text.DecimalFormat("0.00").format(4.025));
输出是4.02
BigDecimal
在《Effective Java》这本书中也提到这个原则,float和double只能用来做科学计算或者是工程计算,在商业计算中我们要用 java.math.BigDecimal。BigDecimal一共有4个够造方法,我们不关心用BigInteger来够造的那两个,那么还有两个,它们是:
BigDecimal(double val)
Translates a double into a BigDecimal.
BigDecimal(String val)
Translates the String repre sentation of a BigDecimal into a BigDecimal.
上面的API简要描述相当的明确,而且通常情况下,上面的那一个使用起来要方便一些。我们可能想都不想就用上了,会有什么问题呢?等到出了问题的时候,才发现上面哪个够造方法的详细说明中有这么一段:
Note: the results of this constructor can be somewhat unpredictable. One might assume that new BigDecimal(.1) is exactly equal to .1, but it is actually equal to .1000000000000000055511151231257827021181583404541015625. This is so because .1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the long value that is being passed in to the constructor is not exactly equal to .1, appearances nonwithstanding.
The (String) constructor, on the other hand, is perfectly predictable: new BigDecimal(".1") is exactly equal to .1, as one would expect. Therefore, it is generally recommended that the (String) constructor be used in preference to this one.
原来我们如果需要精确计算,非要用String来够造BigDecimal不可!在《Effective Java》一书中的例子是用String来够造BigDecimal的,但是书上却没有强调这一点,这也许是一个小小的失误吧。
解决方案
现在我们已经可以解决这个问题了,原则是使用BigDecimal并且一定要用String来够造。
但是想像一下吧,如果我们要做一个加法运算,需要先将两个浮点数转为String,然后够造成BigDecimal,在其中一个上调用add方法,传入另一个作为参数,然后把运算的结果(BigDecimal)再转换为浮点数。你能够忍受这么烦琐的过程吗?下面我们提供一个工具类Arith来简化操作。它提供以下静态方法,包括加减乘除和四舍五入:
public static double add(double v1,double v2)
public static double sub(double v1,double v2)
public static double mul(double v1,double v2)
public static double div(double v1,double v2)
public static double div(double v1,double v2,int scale)
public static double round(double v,int scale)
附录
源文件Arith.java:
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 BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2).doubleValue();
}
/**
* 提供精确的减法运算。
* @param v1 被减数
* @param v2 减数
* @return 两个参数的差
*/
public static double sub(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2).doubleValue();
}
/**
* 提供精确的乘法运算。
* @param v1 被乘数
* @param v2 乘数
* @return 两个参数的积
*/
public static double mul(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
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 BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(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 BigDecimal(Double.toString(v));
BigDecimal one = new BigDecimal("1");
return b.divide(one,scale,BigDecimal.ROUND_HALF_UP).doubleValue();
}
}
http://blog.csdn.net/pttaag/article/details/5912171
最近在项目中碰到了一个业务逻辑计算,代码如下(示例代码)
double val1 = ...;
double val2 = ...,
double dif = ...,
if (Math.abs(val1 - val2-dif) == 0){
//do things
}
结果发现有一组数据:61.5,60.4,1.1无法达到正确的结果.有经验的开发人员一眼就可以发现问题所在,也知道应该采用如下的方式修改代码(产品模式下要进行代码的抽取和封装):
double exp = 10E-10;
if (Math.abs(val1 - val2-dif)>-1*exp && Math.abs(val1 - val2-dif)<exp){
//do things
}
那为什么上面代码中"Math.abs(val1 - val2-dif) == 0"的值为什么会是false呢?这就引申到java的一个基础问题,即java中浮点数的存储机制.
Java 中的浮点数分为单精度和双精度数,也就是float和double.
float在内存中跟int一样,占4个字节,32 bit.
第1个bit表示符号,0表示正数,1表示负数,这个很好理解,不用多管.
第2-9个bit表示指数,一共8位(可以表示0-255),这里的底数是2,为了同时表示正数和负数,这里要减去127的偏移量.这样的话范围就是(-127到128),
另外全0和全1作为特殊处理,所以直接表示-126到127.
剩下的23位表示小数部分,这里23位表示了24位的数字,因为有一个默认的前导1(只有二进制才有这个特性).
最后结果是:(-1)^(sign) * 1.f * 2^(exponent)
这里:sign是符号位,f是23bit的小数部分,exponent是指数部分,最后表示范围是(因为正负数是对称的,这里只关心正数)
2^(-126) ~~ 2(1-2^(-24)) * 2^127
这个还不是float的取值范围,因为标准中还规定了非规格化表示法,另外还有一些特殊规定.
非规格化表示:
当指数部分全0而且小数部分不全0时表示的是非规格化的浮点数,因为这里默认没有前导1,而是0.
取值位0.f * 2^(-126),表示范围位 2^(-149)~~ (1-2^(-23)) * 2^(-126) 这里没有考虑符号.这里为什么是-126而不是-127? 如果是-127的话,那么最大表示为
2^(-127)-2^(-149),很显然2^(-127) ~~2^(-126) 就没法表示了.
其他特殊表示
1.当指数部分和小数部分全为0时,表示0值,有+0和-0之分(符号位决定),0x00000000表示正0,0x80000000表示负0.
2.指数部分全1,小数部分全0时,表示无穷大,有正无穷和负无穷,0x7f800000表示正无穷,0xff800000表示负无穷.
3.指数部分全1,小数部分不全0时,表示NaN,分为QNaN和SNaN,Java中都是NaN.
结论:
可以看出浮点数的取值范围是:2^(-149)~~(2-2^(-23))*2^127,也就是Float.MIN_VALUE和Float.MAX_VALUE.
References:
http://blog.csdn.net/treeroot/archive/2004/09/05/95071.aspx
http://hi.baidu.com/520miner/blog/item/698266ed9ee000d7b31cb1aa.html
http://blog.csdn.net/running8063/article/details/4093261
背景
在博客 恶心的0.5四舍五入问题 一文中看到一个关于 0.5 不能正确的四舍五入的问题。主要说的是 double 转换到 BigDecimal 后,进行四舍五入得不到正确的结果:
public class BigDecimalTest {
public static void main(String[] args){
double d = 301353.05;
BigDecimal decimal = new BigDecimal(d);
System.out.println(decimal);//301353.0499999999883584678173065185546875
System.out.println(decimal.setScale(1, RoundingMode.HALF_UP));//301353.0
}
}
输出的结果为:
301353.0499999999883584678173065185546875
301353.0
这个结果显然不是我们所期望的,我们希望的是得到 301353.1 。
原因
允许明眼人一眼就看出另外问题所在——BigDecimal的构造函数 public BigDecimal(double val) 损失了double 参数的精度,最后才导致了错误的结果。所以问题的关键是:BigDecimal的构造函数 public BigDecimal(double val) 损失了double 参数的精度。
解决之道
因为上面找到了原因,所以也就很好解决了。只要防止了 double 到 BigDecimal 的精度的损失,也就不会出现问题。
1)很容易想到第一个解决办法:使用BigDecimal的以String为参数的构造函数:public BigDecimal(String val) 来替代。
public class BigDecimalTest {
public static void main(String[] args){
double d = 301353.05;
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal("301353.05"));
System.out.println(new BigDecimal("301353.895898895455898954895989"));
}
}
输出结果:
301353.05
301353.05
301353.895898895455898954895989
我们看到了没有任何的精度损失,四舍五入也就肯定不会出错了。
2)BigDecimal的构造函数 public BigDecimal(double val) 会损失了double 参数的精度,这个也许应该可以算作是 JDK 中的一个 bug 了。既然存在bug,那么我们就应该解决它。上面的办法是绕过了它。现在我们实现自己的 double 到 BigDecimal 的转换,并且保证在某些情况下可以完全不损失 double 的精度。
import java.math.BigDecimal; public class BigDecimalUtil { public static BigDecimal doubleToBigDecimal(double d){
String doubleStr = String.valueOf(d);
if(doubleStr.indexOf(".") != -1){
int pointLen = doubleStr.replaceAll("\\d+\\.", "").length(); // 取得小数点后的数字的位数
pointLen = pointLen > 16 ? 16 : pointLen; // double最大有效小数点后的位数为16
double pow = Math.pow(10, pointLen); long tmp = (long)(d * pow);
return new BigDecimal(tmp).divide(new BigDecimal(pow));
}
return new BigDecimal(d);
} public static void main(String[] args){
// System.out.println(doubleToBigDecimal(301353.05));
// System.out.println(doubleToBigDecimal(-301353.05));
// System.out.println(doubleToBigDecimal(new Double(-301353.05)));
// System.out.println(doubleToBigDecimal(301353));
// System.out.println(doubleToBigDecimal(new Double(-301353))); double d = 301353.05;//5898895455898954895989;
System.out.println(doubleToBigDecimal(d));
System.out.println(d);
System.out.println(new Double(d).toString());
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal(d));
}
}
输出结果:
301353.05
301353.05
301353.05
301353.05
301353.0499999999883584678173065185546875
上面我们自己写了一个工具类,实现了 double 到 BigDecimal 的“无损失”double精度的转换。方法是将小数点后有有效数字的double先转换到小数点后没有有效数字的double,然后在转换到 BigDecimal ,之后使用BigDecimal的 divide 返回之前的大小。
上面的结果看起来好像十分的完美,但是其实是存在问题的。上面我们也说到了“某些情况下可以完全不损失 double 的精度”,我们先看一个例子:
public static void main(String[] args){
double d = 301353.05;
System.out.println(doubleToBigDecimal(d));
System.out.println(d);
System.out.println(new Double(d).toString());
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal(d)); System.out.println("=========================");
d = 301353.895898895455898954895989;
System.out.println(doubleToBigDecimal(d));
System.out.println(d);
System.out.println(new Double(d).toString());
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal(d));
System.out.println(new BigDecimal("301353.895898895455898954895989")); System.out.println("=========================");
d = 301353.46899434;
System.out.println(doubleToBigDecimal(d));
System.out.println(d);
System.out.println(new Double(d).toString());
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal(d)); System.out.println("=========================");
d = 301353.45789666;
System.out.println(doubleToBigDecimal(d));
System.out.println(d);
System.out.println(new Double(d).toString());
System.out.println(new BigDecimal(new Double(d).toString()));
System.out.println(new BigDecimal(d));
}
输出结果:
301353.05
301353.05
301353.05
301353.05
301353.0499999999883584678173065185546875
=========================
301353.89589889544
301353.89589889545
301353.89589889545
301353.89589889545
301353.895898895454593002796173095703125
301353.895898895455898954895989
=========================
301353.46899434
301353.46899434
301353.46899434
301353.46899434
301353.4689943399862386286258697509765625
=========================
301353.45789666
301353.45789666
301353.45789666
301353.45789666
301353.4578966600238345563411712646484375
我们可以看到:我们自己实现的 doubleToBigDecimal 方法只有在 double 的小数点后的数字位数比较少时(比如只有5,6位),才能保证完全的不损失精度。
在 double 的小数点后的数字位数比较多时,d * pow 会存在精度损失,所以最终的结果也会存在精度损失。所以如果小数点后的位数比较多时,还是使用 BigDecimal的 String 参数的构造函数为好,只有在小数点后的位数比较少时,才可以采用自己实现的 doubleToBigDecimal 方法。
因为我们看到原始的double的转换之后的BigDecimal的数字的最后一位一个时5,一个是4,原因是在上面的转换方法中:
long tmp = (long)(d * pow);
这一步可能存在很小的精度损失,因为 d 是一个 double ,d * pow 之后还是一个 double(但是小数点之后都是0了,所以到long的转换没有精度损失) ,所以会存在很小的精度损失(double的计算总是有可能存在精度损失的)。但是这个精度损失和 BigDecimal的构造函数 public BigDecimal(double val) 的精度损失相比而言,不会显得那么的突兀(也许我们自己写的doubleToBigDecimal也是存在问题的,欢迎指点)。
总结:
如果需要保证精度,最好是不要使用BigDecimal的double参数的构造函数,因为存在损失double参数精度的可能,最好是使用BigDecimal的String参数的构造函数。最好是杜绝使用BigDecimal的double参数的构造函数。
后记:
其实说这是BigDecimal的一个bug,有标题党的嫌疑,最多可以算作是BigDecimal的一个“坑”。
http://www.cnblogs.com/digdeep/p/4459781.html
恶心的0.5四舍五入问题
四舍五入是财务类应用中常见的需求,按中国人的财务习惯,遇到0.5统一向上进位,但是c#与java中默认的却不是这样。
见c#代码:
1 static void Main(string[] args)
2 {
3 Decimal d = 301353.05M;
4 Console.WriteLine(d);//301353.05
5 Console.WriteLine(Math.Round(d, 1));//301353.0
6 Console.WriteLine(Math.Round(d, 1, MidpointRounding.AwayFromZero));//301353.1
7
8 Console.ReadKey();
9 }
默认情况下,如果要舍弃的位置上,正好值是5,系统会看前一位是奇数还是偶数,如果是偶数,则丢弃最后1位,即上面代码行5,输出的结果为 301353.0,这不符合国人的习惯,所以要人为指定第3个参数"MidpointRounding.AwayFromZero"
java中也提出了类似的做法,但是有“缺陷”
1 @Test
2 public void testScale(){
3 double d = 301353.05;
4 BigDecimal decimal = new BigDecimal(d);
5 System.out.println(decimal);//301353.0499999999883584678173065185546875
6 System.out.println(decimal.setScale(1, RoundingMode.HALF_UP));//301353.0
7 }
类似的,在设置精度时,可以指定一个额外的参数RoundingMode.HALF_UP,表示如果要舍弃的这一位正好是5,则向上进位,代码看似没有问题,但是输出值却是301353.0
原因在于BigDecimal在计算机内部的存储值为"301353.0499999999883584678173065185546875",即小数点第2位是4,上面的代码要求精度到1位,所以代码执行时,只看第2个小数位,其值为4,没有到HALF的标准,因此直接扔掉
改进方法:
1 @Test
2 public void testScale(){
3 double d = 301353.05 + 0.0000000001;
4 BigDecimal decimal = new BigDecimal(d);
5 System.out.println(decimal);//301353.0500000001047737896442413330078125
6 System.out.println(decimal.setScale(1, RoundingMode.HALF_UP));//301353.1
7 }
在满足财务精度的前提下,将要处理的数字加1个微小的偏移量,这样计算机内部存储时,值变成301353.0500000001047737896442413330078125,这样小数位第2位变成了5,满足了HALF_UP的条件。
http://www.cnblogs.com/yjmyzz/p/4427669.html
使用下面的构造函数就不会导致精度损失了:
BigDecimal decimal = new BigDecimal(String.valueOf(d));
或者
BigDecimal decimal = new BigDecimal(new Double(d).toString());
这是BigDecimal的一个坑。
java中浮点数的比较(double, float)(转)的更多相关文章
-
Java 中浮点数---------BigDecimal和double(初探)
为什么要使用 bigdecimal? 借用<Effactive Java>这本书中的话,float和double类型的主要设计目标是为了科学计算和工程计算.他们执行二进制浮点运算,这是为了 ...
-
计算价格, java中浮点数精度丢失的解决方案
计算价格, java中浮点数精度丢失的解决方案
-
Java中浮点数能连续精确表示整数的范围
转自http://blog.csdn.net/seizef/article/details/5571783#ref_1,有删改. 先简单介绍一下浮点数在计算机中的组成,在Java中采用的浮点数表示法是 ...
-
Java中浮点数的精度问题 【转】
当您在计算Money的时候,请看好了!!!要不损失了别后悔!!! 现象1: public static void main(String[] args) { System.out.println(0. ...
-
Java中的long与double的区别
1.long与double在java中本身都是用64位存储的,但是他们的存储方式不同,导致double可储存的范围比long大很多 2.long可以准确存储19位数字,而double只能准备存储16位 ...
-
Java中浮点数的基础知识
偶然查看Math.round的JDK public static int round(float a) { if (a != 0x1.fffffep-2f) // greatest float val ...
-
Java中浮点数的坑
基本数据类型 浮点数存在误差 浮点数有一个需要特别注意的点就是浮点数是有误差的,比如以下这段代码你觉得输出的什么结果: public class Demo { public static void m ...
-
java第二周的学习知识4(对原码,补码,反码和java中浮点数计算不准确的总结)
原码:一个正数,转换为二进制位就是这个正数的原码.负数的绝对值转换成二进制位然后在高位补1就是这个负数的原码. 但是原码有几个缺点,零分两种 +0 和 -0 .很奇怪是吧!还有,在进行不同符号的加法运 ...
-
为什么Java中Long类型的比float类型的范围小?
为什么Long类型的比float类型的范围小? 2015-09-15 22:36 680人阅读 评论(0) 收藏 举报 版权声明:本文为博主原创文章,未经博主允许不得转载. 作为一个常识,我们都知道浮 ...
随机推荐
-
HoloLens开发手记 - Vuforia开发概述 Vuforia development overview
关于Vuforia,开发AR应用的人基本都会熟悉.之前我也写过一篇关于Vuforia开发的博客:Vuforia AR SDK入门 今天这篇博客则主要是谈谈HoloLens使用Vuforia开发混合现实 ...
-
Effective Java 73 Avoid thread groups
Thread groups were originally envisioned as a mechanism for isolating applets for security purposes. ...
-
使用fsck修复文件系统错误
1.问题描述 服务器maint_samba 由于服务器maint_samba (debian操作系统)没有正常关机,在重新启动过程中/dev/sdb1出现文件系统错误,需要手动使用fsck进行扫描 ...
-
spring 学习笔记1
Spring 学习记录 任何一个成功的应用都是由多个为了实现某一个业务目标而相互协作的组件构成的.这些组件必须彼此了解,并相互协作来完成工作. 在Spring 中,对象无需自己负责查找或创建与其关联的 ...
-
css3新特性合集
转自:https://www.cnblogs.com/xiaoxie2016/p/5964694.html (若原作者对此转载有疑问,联系删除,谢谢!) animation IE10 anima ...
-
通过ModelForm实现主机添加和编辑
通过ModelForm实现主机添加和编辑 ModelForm这是一个神奇的组件,通过名字我们可以看出来,这个组件的功能就是把model和form组合起来:在使用Model和Form时,都需要对字段进行 ...
-
ToolBar样式颜色,图标设置
extends:http://blog.csdn.net/w1054993544/article/details/48339565 <resources> <style name=& ...
-
GeoServer服务器环境的搭建
.java 的安装 下载地址:http://www.oracle.com/technetwork/java/javase/downloads/index.html 我下载的java 1.8版本 1. ...
-
【Python】python3-list列表引用
print(names) #列出列表的内容 print(names[3]) #访问列表中第4个值 print(names[1:3]) #访问列表中从第2个到第3个的值 print(names[-1]) ...
-
20155321 2016-2017-2 《Java程序设计》第六周学习总结
20155321 2016-2017-2 <Java程序设计>第六周学习总结 教材学习内容总结 第十章 IO 流 IO 流用来处理设备之间的数据传输 Java对数据的操作是通过流的方式 J ...