浮点数的精度问题
我们首先看一个例子:
System.out.println(0.1 +0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);
double amount1 = 2.15;
double amount2 = 1.10;
if (amount1 - amount2 == 1.05)
System.out.println("OK");
我们预测的结果:
System.out.println(0.1 +0.2);// 0.3
System.out.println(1.0-0.8);// 0.2
System.out.println(4.015*100);//401.5
System.out.println(123.3/100)//1.233
OK
而真正的结果呢?
0.30000000000000004
0.19999999999999996
401.49999999999994
1.2329999999999999
那么为什么会出现上述的结果呢?
因为Java浮点数使用二进制数据的科学计数法来表示浮点数,因此可能不能精确表示一个浮点数。
比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
如果在一些精度要求很高的系统中,这种问题是非常严重的,因为如果你有9.99999999元,计算机是不会问题你可以购买10元商品的。特别是做金钱交易的时候,最终损失的精度就是大量的资金出入的。
在<> 一书中到了一个原则,那就是float 和 double 只能用来科学计算或者工程计算,但是在商业计算中我们要用 java.math.BigDecimal
。通过使用BigDecimal 类可以解决上面问题。
我们以相加为例:
double d1 = 0.1;
double d2 = 0.2;
BigDecimal b1 = new BigDecimal(Double.toString(d1));
BigDecimal b2 = new BigDecimal(Double.toString(d2));
System.out.println(b1.add(b2).doubleValue());
错误用法如下:
double d1 = 0.1;
double d2 = 0.2;
BigDecimal b1 = new BigDecimal(d1);
BigDecimal b2 = new BigDecimal(d2);
System.out.println(b1.add(b2).doubleValue());
得到的结果:0.30000000000000004
可以看到,运算结果还是不精确,只不过是精度高了而已。这时因为 BigDecimal(doubel var) 构造方法的结果有一定的不可预知性。有人认为在java中写入new BigDecimal(0.1) 所创建的BigDecimal 正好等于0.1,但是它实际等于 0.1000000000005555533377873773. 这时因为0.1 无法准确地表示为double ,这样传入到构造方法的值不会正好等于0.1。
Stirng 了你行难呢过参数的构造方法是完全可预知的:写入 new BigDecimal(“0.1”) 将创建一个BigDecimal, 它正好等于预期的0.1.因此,比较而言,通常建议优先使用String 类型参数的构造方法。
第一个坑 : 使用BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化BigDecimal
浮点数的舍入和格式化的方式
String.format 的格式化舍入方式,结果可能也是你预想不到的,我们看下面的例子:
double num1 = 3.35;
float num2 = 3.35f;
System.out.println(String.format("%.1f", num1));//四舍五入
System.out.println(String.format("%.1f", num2));
结果竟然是:
3.4
3.3
那么为什么呢?
因为这时因为精度问题和舍入方式共同导致的,double 和 float 的3.35 想当于 3.350xxx 和 3.349xxx。
3.350000000000000088817841970012523233890533447265625
3.349999904632568359375
String.format 采用四舍五入的方式进行舍入,取 1 位小数,double 的 3.350 四舍五入为 3.4,而 float 的 3.349 四舍五入为 3.3。
我们看一下Formatter 类的相关源码,可以发现使用的舍入模式是 HALF_UP
else if (c == Conversion.DECIMAL_FLOAT) {
// Create a new BigDecimal with the desired precision.
int prec = (precision == -1 ? 6 : precision);
int scale = value.scale();
if (scale > prec) {
// more "scale" digits than the requested "precision"
int compPrec = value.precision();
if (compPrec <= scale) {
// case of 0.xxxxxx
value = value.setScale(prec, RoundingMode.HALF_UP);
} else {
compPrec -= (scale - prec);
value = new BigDecimal(value.unscaledValue(),
scale,
new MathContext(compPrec));
}
}
如果我们呢希望使用其他舍入方式来格式化字符串的话,可以设置 DecimalFormatter,如下代码所示:
double num1 = 3.3555;
float num2 = 3.35f;
DecimalFormat format = new DecimalFormat("#.##");
format.setRoundingMode(RoundingMode.FLOOR);
//format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num1));
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num2));
DecimalFormatter 其他用法如下:
double pi = 3.1415927;//圆周率
//取一位整数
System.out.println(new DecimalFormat("0").format(pi));//3
//取一位整数和两位小数
System.out.println(new DecimalFormat("0.00").format(pi));//3.14
//取两位整数和三位小数,整数不足部分以0填补。
System.out.println(new DecimalFormat("00.000").format(pi));// 03.142
//取所有整数部分
System.out.println(new DecimalFormat("#").format(pi));//3
//以百分比方式计数,并取两位小数
System.out.println(new DecimalFormat("#.##%").format(pi));//314.16%
long c =299792458;//光速
//显示为科学计数法,并取五位小数
System.out.println(new DecimalFormat("#.#####E0").format(c));//2.99792E8
//显示为两位整数的科学计数法,并取四位小数
System.out.println(new DecimalFormat("00.####E0").format(c));//29.9792E7
//每三位以逗号进行分隔。
System.out.println(new DecimalFormat(",###").format(c));//299,792,458
//将格式嵌入文本
System.out.println(new DecimalFormat("光速大小为每秒,###米。").format(c));
第二个坑: 浮点数的字符串格式化也要通过BigDecimal进行
BigDecimal 怎样比较相等
我们知道包装类的比较要通过equals进行,而不能使用 ==。我们使用一个例子如下:
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")))
得到的结果时false. 我们看下BigDecimal 的equals 方法
/**
* Compares this {@code BigDecimal} with the specified
* {@code Object} for equality. Unlike {@link
* #compareTo(BigDecimal) compareTo}, this method considers two
* {@code BigDecimal} objects equal only if they are equal in
* value and scale (thus 2.0 is not equal to 2.00 when compared by
* this method).
*
* @param x {@code Object} to which this {@code BigDecimal} is
* to be compared.
* @return {@code true} if and only if the specified {@code Object} is a
* {@code BigDecimal} whose value and scale are equal to this
* {@code BigDecimal}'s.
* @see #compareTo(java.math.BigDecimal)
* @see #hashCode
*/
上面的注释说明,equals 比较的是两个参数 一个是value 一个 是scale
即 2.0 是不等于 2.00 的。
如果我们只是比较值的话,可以使用compareTo方法
System.out.println(new BigDecimal("2.0").compareTo(new BigDecimal("2.00"))==0);
结果为 true
小心值的溢出
数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性。
比如,对 Long 的最大值进行 +1 操作:
long l = Long.MAX_VALUE;
System.out.println(l + 1);
System.out.println(l + 1 == Long.MIN_VALUE);
结果:
-9223372036854775808
true
结果 为负数,因为long溢出了,
虽然发生了溢出,但是没有提示任何异常。这类问题容易被忽略,改进的方式有以下两种:
方法一是,考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法进行数值运算,这些方法可以在数值溢出时主动抛出异常。我们来测试一下,使用 Math.addExact 对 Long 最大值做 +1 操作:
try {
long l = Long.MAX_VALUE;
System.out.println(Math.addExact(l, 1));
} catch (Exception ex) {
ex.printStackTrace();
}
执行后,可以得到 ArithmeticException,这是一个 RuntimeException:
java.lang.ArithmeticException: long overflow
at java.lang.Math.addExact(Math.java:809)
at com.md.DateTimeTest.main(DateTimeTest.java:14)
方法二是,使用大数类 BigInteger。BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。
如下代码,使用 BigInteger 对 Long 最大值进行 +1 操作;如果希望把计算结果转换一个 Long 变量的话,可以使用 BigInteger 的 longValueExact 方法,在转换出现溢出时,同样会抛出 ArithmeticException:
BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
System.out.println(i.add(BigInteger.ONE).toString());
try {
long l = i.add(BigInteger.ONE).longValueExact();
} catch (Exception ex) {
ex.printStackTrace();
}
输出结果如下:
9223372036854775808
java.lang.ArithmeticException: BigInteger out of long range
at java.math.BigInteger.longValueExact(BigInteger.java:4531)
at com.md.DateTimeTest.main(DateTimeTest.java:17)
可以看到,通过 BigInteger 对 Long 的最大值加 1 一点问题都没有,当尝试把结果转换为 Long 类型时,则会提示 BigInteger out of long range。
总结
第一,切记,要精确表示浮点数应该使用 BigDecimal。并且,使用 BigDecimal 的 Double 入参的构造方法同样存在精度丢失问题,应该使用 String 入参的构造方法或者 BigDecimal.valueOf 方法来初始化。
第二,对浮点数做精确计算,参与计算的各种数值应该始终使用 BigDecimal,所有的计算都要通过 BigDecimal 的方法进行,切勿只是让 BigDecimal 来走过场。任何一个环节出现精度损失,最后的计算结果可能都会出现误差。
第三,对于浮点数的格式化,如果使用 String.format 的话,需要认识到它使用的是四舍五入,可以考虑使用 DecimalFormat 来明确指定舍入方式。但考虑到精度问题,我更建议使用 BigDecimal 来表示浮点数,并使用其 setScale 方法指定舍入的位数和方式。
第四,进行数值运算时要小心溢出问题,虽然溢出后不会出现异常,但得到的计算结果是完全错误的。我们考虑使用 Math.xxxExact 方法来进行运算,在溢出时能抛出异常,更建议对于可能会出现溢出的大数运算使用 BigInteger 类。
总之,对于金融、科学计算等场景,请尽可能使用 BigDecimal 和 BigInteger,避免由精度和溢出问题引发难以发现,但影响重大的 Bug。