java⼩数是怎么运算的_JAVA中⼩数的运算
虽然⼏乎每种处理器和编程语⾔都⽀持浮点运算,但⼤多数程序员很少注意它。这容易理解
―
我们中⼤多数很少需要使⽤⾮整数类型。除了科学计算和偶尔的计时测试或基准测试程序,其它情况下⼏乎都⽤不着它。同样,⼤多数开发⼈员也容易忽略
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
位表⽰尾数。
因为⽤科学记数法可以有多种⽅式来表⽰给定数字,所以要规范化浮点数,以便⽤底数为
2 并且⼩数点左边为 1
的⼩数来表⽰,按照需要调节指数就可以得到所需的数字。所以,例如,数
1.25 可以表⽰为尾数为 1.01,指数为 0: (-1) 0*1.01 2*2 0
数 10.0 可以表⽰为尾数为 1.01,指数为 3: (-1) 0*1.01 2*2
3
特殊数字
除了编码所允许的值的标准范围(对于 float ,从 1.4e-45 到
3.4028235e+38),还有⼀些表⽰⽆穷⼤、负⽆穷⼤、 -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 -> ⽆穷⼤
-1.0 / 0.0 -> 负⽆穷⼤
NaN + 1.0 -> NaN
⽆穷⼤ + 1.0 -> ⽆穷⼤
⽆穷⼤ + ⽆穷⼤ -> ⽆穷⼤
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
在表⽰为浮点值时,被认为是相等的,但使⽤ FloatpareTo()
来⽐较作为 Float 对象的 0 和 -0 时,会显⽰ -0 ⼩于 0 。
浮点中的危险
由于⽆穷⼤、NaN 和 0
的特殊⾏为,当应⽤浮点数时,可能看似⽆害的转换和优化实际上是不正确的。例如,虽然好象0.0-f 很明显等于 -f ,但当 f 为 0 时,这是不正确的。还有其它类似的
gotcha,表 2 显⽰了其中⼀些 gotcha。
表 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
的不寻常⽐较⾏为和在⼏乎所有浮点计算中都不可避免地会出现舍⼊误差,解释浮点值的⽐较运算符的结果⽐较⿇烦。
最好完全避免使⽤浮点数⽐较。当然,这并不总是可能的,但您应该意识到要限制浮点数⽐较。如果必须⽐较浮点数来看它们是否相等,则应该将它们差的绝对值同⼀些预先选定的⼩正数进⾏⽐较,这样您所做的就是测试它们是否“⾜够接近”。(如果不知道基本的计算范围,可以使⽤测试“abs(a/b
- 1) <
epsilon”,这种⽅法⽐简单地⽐较两者之差要更准确)。甚⾄测试看⼀个值是⽐零⼤还是⽐零⼩也存在危险
―“以为”会⽣成⽐零略⼤值的计算事实上可能由于积累的舍⼊误差会⽣成略微⽐零⼩的数字。
NaN
的⽆序性质使得在⽐较浮点数时更容易发⽣错误。当⽐较浮点数时,围绕⽆穷⼤和
NaN 问题,⼀种避免 gotcha
的经验法则是显式地测试值的有效性,⽽不是试图排除⽆效值。在清单 1
中,有两个可能的⽤于特性的 setter
的实现,该特性只能接受⾮负数值。第⼀个实现会接受
NaN,第⼆个不会。第⼆种形式⽐较好,因为它显式地检测了您认为有效的值的范围。
清单 1. 需要⾮负浮点值的较好办法和较差办法
// Trying to
test by exclusion -- this doesn't catch NaN or
infinitypublic void setFoo(float foo) {if (foo < 0)throw new String(f));this.foo =
foo; }// Testing by
inclusion -- this does catch NaNpublic void setFoo(float foo) {if (foo >= 0
&& foo < Float.INFINITY)this.foo =
foo;elsethrow new String(f));
}
不要⽤浮点值表⽰精确值
⼀些⾮整数值(如⼏美元和⼏美分这样的⼩数)需要很精确。浮点数不是精确值,所以使⽤它们会导致舍⼊误差。因此,使⽤浮点数来试图表⽰象货币量这样的精确数量不是⼀个好的想法。使⽤浮点数来进⾏美元和美分计算会得到灾难性的后果。浮点数最好⽤来表⽰象测量值这类数值,这类值从⼀开始就不怎么精确。
⽤于较⼩数的 BigDecimal
从 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 会产⽣⽆限循环的⼩数 .
。出于这个原因,在进⾏除法运算时, BigDecimal
可以让您显式地控制舍⼊。 movePointLeft() ⽅法⽀持 10
的幂次⽅的精确除法。
使⽤ BigDecimal 作为互换类型
SQL-92 包括 DECIMAL
数据类型,它是⽤于表⽰定点⼩数的精确数字类型,它可以对⼩数进⾏基本的算术运算。⼀些
SQL 语⾔喜欢称此类型为 NUMERIC 类型,其它⼀些 SQL 语⾔则引⼊了
MONEY 数据类型,MONEY
数据类型被定义为⼩数点右侧带有两位的⼩数。
如果希望将数字存储到数据库中的 DECIMAL 字段,或从 DECIMAL
字段检索值,则如何确保精确地转换该数字?您可能不希望使⽤由 JDBC
PreparedStatement 和 ResultSet 类所提供的 setFloat() 和 getFloat()
⽅法,因为浮点数与⼩数之间的转换可能会丧失精确性。相反,请使⽤
PreparedStatement 和 ResultSet 的 setBigDecimal() 及
getBigDecimal() ⽅法。
对于 BigDecimal
,有⼏个可⽤的构造函数。其中⼀个构造函数以双精度浮点数作为输⼊,另⼀个以整数和换算因⼦作为输⼊,还有⼀个以⼩数的String 表⽰作为输⼊。要⼩⼼使⽤ BigDecimal(double)
构造函数,因为如果不了解它,会在计算过程中产⽣舍⼊误差。请使⽤基于整数或
bigdecimal除法保留小数String 的构造函数。
构造 BigDecimal 数
对于 BigDecimal
,有⼏个可⽤的构造函数。其中⼀个构造函数以双精度浮点数作为输⼊,另⼀个以整数和换算因⼦作为输⼊,还有⼀个以⼩数的String 表⽰作为输⼊。要⼩⼼使⽤ BigDecimal(double)
构造函数,因为如果不了解它,会在计算过程中产⽣舍⼊误差。请使⽤基于整数或
String 的构造函数。
如果使⽤ BigDecimal(double) 构造函数不恰当,在传递给 JDBC
setBigDecimal() ⽅法时,会造成似乎很奇怪的 JDBC
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论