尽管几乎每种处理器和编程语言都支持浮点算术,但大多数程序员对此却很少注意。 这是可以理解的-我们大多数人很少要求使用非整数数字类型。 除了科学计算和偶尔的计时测试或基准测试外,它只是没有出现。 大多数开发人员同样会忽略java.math.BigDecimal
提供的任意精度的十进制数字-绝大多数应用程序都没有使用它们。 但是,代表非整数的变量确实偶尔会潜入其他以整数为中心的程序中。 例如,JDBC使用BigDecimal
作为SQL DECIMAL
列的首选交换格式。
IEEE浮点数
Java语言支持两种原始浮点类型: float
和double
,以及它们的包装类对应的Float
和Double
。 这些基于IEEE 754标准,该标准定义了32位浮点和64位双精度浮点二进制十进制数的二进制标准。
IEEE 754用科学计数法将浮点数表示为以2为基数的十进制数。 IEEE浮点数将1位用于数字的符号,将8位用于指数,将23位用于尾数或小数部分。 指数被解释为有符号整数,允许正负两个指数。 小数表示为二进制(以2为基)的十进制,表示最高位对应于值½(2 -1 ),第二位对应值¼(2 -2 ),依此类推。 对于双精度浮点,指数专用于11位,而尾数专用于52位。 IEEE浮点值的布局如图1所示。
图1. IEEE 754浮点布局
因为任何给定的数字都可以用科学的表示法以多种方式表示,所以对浮点数进行了归一化,以便将它们表示为以2为基数的小数,并在小数点的左侧加上1,并根据需要调整指数以使该要求成立。 。 因此,例如,数字1.25的尾数为1.01,指数为0:
(-1)
数字10.0的尾数为1.01,指数为3:
(-1)
特殊号码
除了标准值的范围允许由编码(从1.4E-45到3.4028235E + 38为float
),有表示无穷大的特殊值,负无穷大, -0
和NaN(其代表“不是一个数”)。 这些值的存在使错误条件(例如算术溢出,取负数的平方根并除以0
可以产生可以在浮点值集中表示的结果。
这些特殊数字具有一些不同寻常的特征。 例如, 0
和-0
是不同的值,但是在进行相等性比较时,它们被视为相等。 将非零数字除以无穷大将得出0
。 特殊数字NaN是无序的; 使用==
, <
和>
运算符在NaN和其他浮点值之间进行任何比较都会产生false
。 如果f
为NaN,则偶数(f == f)
也会得出false
。 如果要将浮点值与NaN进行比较,请改用Float.isNaN()
方法。 表1显示了无限和NaN的一些特性。
表1.特殊浮点值的属性
表达 | 结果 |
---|---|
Math.sqrt(-1.0) | -> NaN |
0.0 / 0.0 | -> NaN |
1.0 / 0.0 | -> Infinity |
-1.0 / 0.0 | -> -Infinity |
NaN + 1.0 | -> NaN |
Infinity + 1.0 | -> Infinity |
Infinity + Infinity | -> Infinity |
NaN > 1.0 | -> false |
NaN == 1.0 | -> false |
NaN < 1.0 | -> false |
NaN == NaN | -> false |
0.0 == -0.01 | -> true |
原始浮点类型和包装类浮点具有不同的比较行为
更糟的是,在原始float
类型和包装类Float
之间,用于比较NaN和-0
的规则不同。 对于float
值,比较两个NaN值是否相等将产生false
,但是使用Float.equals()
比较两个NaN Float
对象将产生true
。 这样做的动机是,否则将不可能使用NaN Float
对象作为HashMap
的键。 类似地,虽然0
和-0
在表示为float值时被视为相等,但使用Float.compareTo()
将0
和-0
作为Float
对象进行Float.compareTo()
表明-0
被视为小于0
。
浮点危害
由于无穷大,NaN和0
的特殊行为,当应用于浮点数时,某些可能无害的变换和优化实际上是不正确的。 例如,虽然0.0-f
和-f
等效似乎很明显,但是当f
为0
时,这不是正确的。 还有其他类似的陷阱,表2中显示了其中一些。
表2.无效的浮点假设
这个表情... | 不一定与此相同... | 什么时候... |
---|---|---|
0.0 - f | -f | f为0 |
f < g | ! (f >= g) | f或g为NaN |
f == f | true | f是NaN |
f + g - g | f | g是无穷大或NaN |
舍入误差
浮点运算很少精确。 虽然某些数字(例如0.5
)可以精确地表示为二进制(以2为底)的十进制数(因为0.5
等于2 -1 ),但是其他数字(例如0.1
)则不能。 结果,浮点运算可能会导致舍入错误,产生的结果接近于(但不等于)您可能期望的结果。 例如,下面的简单计算得出2.600000000000001
,而不是2.6
:
double s=0;
for (int i=0; i<26; i++)
s += 0.1;
System.out.println(s);
类似地,将.1*26
乘以得到的结果与将.1
自身加26倍的结果不同。 当从浮点数转换为整数时,舍入误差会变得更加严重,因为转换为整数类型会丢弃非整数部分,即使对于“看起来”它们应该具有整数值的计算。 例如,以下语句:
double d = 29.0 * 0.01;
System.out.println(d);
System.out.println((int) (d * 100));
将产生作为输出:
0.29
28
一开始可能不是您所期望的。
比较浮点数的准则
由于NaN具有异常的比较行为,并且几乎在所有浮点计算中实际上都保证了舍入误差,因此解释比较运算符对浮点值的结果非常棘手。
最好尝试完全避免浮点比较。 当然,这并非总是可能的,但是您应该意识到浮点比较的局限性。 如果必须比较浮点数以查看它们是否相同,则应将其差值的绝对值与某些预先选择的epsilon值进行比较,以便测试它们是否“足够接近”。 (如果您不知道基础测量的规模,则使用测试“ abs(a / b-1)<epsilon”可能比简单比较差异更健壮。)甚至测试一个值以查看是否大于或小于零是有风险的-由于累积的舍入误差,“假定为”计算得出的值略大于零实际上可能导致数字略小于零。
当比较浮点数时,NaN的无序性质增加了进一步的出错机会。 比较浮点数时避开无穷大和NaN周围许多陷阱的一个好的经验法则是显式测试一个值的有效性,而不是尝试排除无效值。 在清单1中,对于属性只能使用非负值的setter,有两种可能的实现。 第一个将接受NaN,第二个将不接受。 第二种形式是可取的,因为它可以显式测试您认为有效的值范围。
清单1.要求float值为非负的更好和更糟糕的方法
// Trying to test by exclusion -- this doesn't catch NaN or infinity
public void setFoo(float foo) {
if (foo < 0)
throw new IllegalArgumentException(Float.toString(f));
this.foo = foo;
}
// Testing by inclusion -- this does catch NaN
public void setFoo(float foo) {
if (foo >= 0 && foo < Float.INFINITY)
this.foo = foo;
else
throw new IllegalArgumentException(Float.toString(f));
}
请勿将浮点数用于确切值
一些非整数值(例如十进制的美元和美分)需要精确度。 浮点数不正确,操作它们将导致舍入错误。 结果,使用浮点数来表示精确的数量(例如货币金额)是一个坏主意。 使用浮点数进行美元和美分的计算是灾难的根源。 浮点数最适合用于测量之类的值,其值从一开始就根本不精确。
小数位数大
从JDK 1.3开始,Java开发人员可以使用另一种非整数的替代方法: BigDecimal
。 BigDecimal
是一个标准类,在编译器中没有特殊支持,它表示任意精度的十进制数并对其执行算术运算。 在内部, BigDecimal
表示为任意精度的未缩放值和比例因子,该比例因子表示将小数点向左移动多少位以获得缩放值。 因此,由BigDecimal
表示的数字是unscaledValue*10 -scale
。
BigDecimal
值的算术由加,减,乘和除方法提供。 由于BigDecimal
对象是不可变的,因此这些方法中的每一个都会产生一个新的BigDecimal
对象。 结果,由于对象创建的开销, BigDecimal
不适用于密集的数字计算,但它旨在表示精确的十进制数。 如果您要表示精确的数量(例如金额), BigDecimal
非常适合此任务。
不是所有的equals方法都相等
像浮点类型一样, BigDecimal
也有一些怪癖。 特别是,请小心使用equals()
方法测试数字相等性。 equals()
方法不会将两个表示相同数字但具有不同比例值(例如100.00
和100.000
)的BigDecimal
值视为相等。 但是, compareTo()
方法将认为它们相等,因此在对两个BigDecimal
值进行数字比较时,应使用compareTo()
而不是equals()
。
在某些情况下,任意精度的十进制算术仍然不足以保持准确的结果。 例如,将1
除以9
得到无限的重复小数.111111...
因此, BigDecimal
使您可以在执行除法运算时对舍入进行显式控制。 movePointLeft()
方法支持精确除以十的幂。
使用BigDecimal作为交换类型
SQL-92包括DECIMAL
数据类型,这是一种精确的数字类型,用于表示定点十进制数字,并对十进制数字执行基本的算术运算。 一些SQL方言更喜欢将此类型称为NUMERIC
,而另一些方言还包含MONEY
数据类型,该数据类型定义为十进制数,在小数点右边有两个位置。
如果要将数字存储到数据库的DECIMAL
字段中,或从DECIMAL
字段中检索值,如何确保数字正确传输? 您不想使用JDBC PreparedStatement
和ResultSet
类提供的setFloat()
和getFloat()
方法,因为浮点数和十进制数之间的转换可能会导致准确性丧失。 而是使用PreparedStatement
和ResultSet
的setBigDecimal()
和getBigDecimal()
方法。
类似地,诸如Castor之类的XML数据绑定工具将使用BigDecimal
为十进制值的属性和元素(在XSD架构中作为基本数据类型支持)生成getter和setter。
构造BigDecimal数
BigDecimal
有几种可用的构造函数。 一个采用双精度浮点作为输入,另一个采用整数和比例因子,另一个采用十进制数字的String
表示。 您应该小心使用BigDecimal(double)
构造函数,因为它可以允许舍入错误在您不知不觉中潜入您的计算中。 而是使用基于整数或基于String
的构造函数。
传递给JDBC setBigDecimal()
方法时, BigDecimal(double)
构造函数使用不当可能会在JDBC驱动程序中显示为奇怪的异常。 例如,考虑以下JDBC代码,该代码希望将数字0.01
存储到十进制字段中:
PreparedStatement ps =
connection.prepareStatement("INSERT INTO Foo SET name=?, value=?");
ps.setString(1, "penny");
ps.setBigDecimal(2, new BigDecimal(0.01));
ps.executeUpdate();
取决于您的JDBC驱动程序,这种看似无害的代码在执行时可能会引发一些令人困惑的异常,因为双精度近似值0.01
会导致较大的比例值,这可能会使JDBC驱动程序或数据库感到困惑。 该异常将起源于JDBC驱动程序,但除非您知道二进制浮点数的局限性,否则可能几乎无法说明代码的实际错误。 而是使用BigDecimal("0.01")
或BigDecimal(1, 2)
BigDecimal("0.01")
构造BigDecimal
以避免出现此问题,因为这两个方法中的任何一个都将导致精确的十进制表示形式。
摘要
在Java程序中使用浮点数和十进制数充满了陷阱。 浮点数和十进制数的表现不如整数好,并且您不能假设“应该”具有整数或精确结果的浮点计算实际上就可以做到。 最好保留使用浮点算法进行涉及根本不精确值的计算,例如测量。 如果需要表示定点数,例如美元和美分,请改用BigDecimal
。
翻译自: https://www.ibm.com/developerworks/java/library/j-jtp0114/index.html