实数是非常密集的。任意两个不同的实数中间都可以出现其他实数。但浮点数则不是这样。对于浮点数和双精度数,也存在下一个浮点数;连续的浮点数和双精度数之间存在最小的有限距离。nextUp()
方法返回比第一个参数大的最近浮点数。例如,清单 2 打印出所有在 1.0 和 2.0 之间的浮点数:
public static void main(String[] args) {
float x = 1.0F;
int numFloats = 0;
while (x <= 2.0) {
numFloats++;
System.out.println(x);
x = Math.nextUp(x);
}
System.out.println(numFloats);
}
}
结果是 1.0 和 2.0 之间包含 8,388,609 个浮点数;虽然很多,但还不至于是无穷多的实数。相邻数字的距离为 0.0000001。这个距离称为?ULP,它是?最小精度单位(unit of least precision)或?最后位置单位(unit in the last place)的缩略。
如果需要向后查找小于指定数字的最近浮点数,则可以改用?nextAfter()
方法。第二个参数指定是否查找在第一个参数之上或之下的最近数字:
如果?direction
大于?start
,则?nextAfter()
返回在?start
之上的下一个数字。如果?direction
小于?start
,则?nextAfter()
返回在?start
之下的下一个数字。如果?direction
等于?start
,则?nextAfter()
返回?start
本身。
这些方法在某些建模或图形工具中是非常有用的。从数字上来说,您可能需要在?a和?b之间的 10,000 个位置上提取样例值,但如果您具备的精度仅能识别?a和?b之间的 1,000 个独立的点,那么有十分之九的工作是重复的。您可以只做十分之一的工作,但又获得相同的结果。
当然,如果一定需要额外的精度,则可以选择具有高精度的数据类型,比如?double
或?BigDecimal
。例如,我曾经在 Mandelbrot 集合管理器看见过这种情况。在其中可以放大曲线图,让其落在最近的两个双精度数之间。Mandelbrot 集合在各个级别上都是非常细微和复杂的,但是float
或?double
可以在失去区分相邻点的能力之前达到这个细微的级别。
Math.ulp()
返回一个数字和距其最近的数字之间的距离。清单 3 列出了 2 的各种幂次方的 ULP:
public static void main(String[] args) {
for (float x = 1.0f; x <= Float.MAX_VALUE; x *= 2.0f) {
System.out.println(Math.getExponent(x) + "t" + x + "t" + Math.ulp(x));
}
}
}
下面给出了一些输出:
可以看到,对于比较小的 2 的幂次方,浮点数是非常精确的。但是在许多应用程序中,在数值约为 220时,这一精度将出现问题。在接近浮点数的最大极限时,相邻的值将被?千的七乘方(sextillions)隔开(事实上可能更大一点,但我找不到词汇来表达)。
如清单 3 所示,ULP 的大小并不是固定的。随着数字变大,它们之间的浮点数就会越来越少。例如,10,000 和 10,001 之间只有 1,025 个浮点数;它们的距离是 0.001。在 1,000,000 和 1,001 之间仅有 17 个浮点数,它们的距离是 0.05。精度与数量级成反比关系。对于浮点数 10,000,ULP 的精确度变为 1.0,超过这个数之后,将有多个整数值映射到同一个浮点数。对于双精度数,只有达到 4.5E15 时才会出现这种情况,但这也是个问题。
Math.ulp()
为测试提供一个实用的用途。很明显,我们一般不会比较两个浮点数是否完全相等。相反,我们检查它们是否在一定的容错范围内相等。例如,在 JUnit 中,像以下这样比较预期的实际浮点值:
这表明实际值与预期值的偏差在 0.02 之内。但是,0.02 是合理的容错范围吗?如果预期值是 10.5 或 -107.82,则 0.02 是完全可以接受的。但当预期值为几十亿时,0.02 则与 0 没有什么区别。通常,就 ULP 进行测试时考虑的是相对错误。一般选择的容错范围在 1 至 10 ULP 之间,具体情况取决于计算所需的精度。例如,下面指定实际结果必须在真实值的 5 个 ULP 之内:
根据期望值不同,这个值可以是万亿分之一,也可以是数百万。
scalb
Math.scalb(x,y)
用 2y乘以 x,scalb
是 “scale binary(二进法)” 的缩写。
例如,Math.scalb(3,4)
返回 3 * 24,即 3*16,结果是 48.0。也可以使用?Math.scalb()
来实现?getMantissa()
:
Math.scalb()
和?x*Math.pow(2,scaleFactor)
的区别是什么?实际上,最终的结果是一样的。任何输入返回的值都是完全一样的。不过,性能方面则存在差别。Math.pow()
的性能是非常糟糕的。它必须能够真正处理一些非常少见的情况,比如对 3.14 采用幂 -0.078。对于小的整数幂,比如 2 和 3(或以 2 为基数,这比较特殊),通常会选择完全错误的算法。
我担心这会对总体性能产生影响。一些编译器和 VM 的智能程度比较高。一些优化器会将?x*Math.pow(2,y)
识别为特殊情况并将其转换为Math.scalb(x,y)
或类似的东西。因此性能上的影响体现不出来。不过,我敢保证有些 VM 是没有这么智能的。例如,使用 Apple 的 Java 6 VM 进行测试时,Math.scalb()
几乎总是比?x*Math.pow(2,y)
快两个数量级。当然,这通常不会造成影响。但是在特殊情况下,比如执行数百万次求幂运算时,则需要考虑能否转换它们以使用?Math.scalb()
。
Copysign
Math.copySign()
方法将第一个参数的标记设置为第二个参数的标记。最简单的实现如清单 4 所示:
copysign算法
0) {
if (magnitude < 0) return -magnitude;
else return magnitude;
}
return magnitude;
}
不过,真正的实现如清单 5 所示:
sun.misc.FpUtils的真正算法
仔细观察 这些位就会看到,NaN 标记被视为正的。严格来说,Math.copySign()
并不对此提供保证,而是由?StrictMath.copySign()
负责,但在现实中,它们都调用相同的位处理代码。
清单 5 可能会比清单 4 快一些,但它的主要目的是正确处理负 0。Math.copySign(10,-0.0)
返回 -10,而?Math.copySign(10,0.0)
返回 10.0。清单 4 中最简单形式的算法在两种情况中都返回 10.0。当执行敏感的操作时,比如用极小的负双精度数除以极大的正双精度数,就可能出现负 0。例如,-1.0E-147/2.1E189
返回负 0,而?1.0E-147/2.1E189
返回正 0。不过,使用?==
比较这两个值时,它们是相等的。因此,如果要区分它们,必须使用?Math.copySign(10,-0.0)
或?Math.signum()
(调用?Math.copySign(10,-0.0)
)来执行比较。
指数函数是一个很好的例子,它表明处理有限精度浮点数(而不是无限精度实数)时是需要非常小心的。在很多等式中都出现ex(Math.exp()
)。例如,它可用于定义 cosh 函数,这已经在?讨论:
cosh(x) = (ex+?e-x)/2
不过,对于负值的 x,一般是 -4 以下的数字,用于计算?Math.exp()
的算法表现很差,并且容易出现舍入错误。使用另一个算法计算?ex- 1 会更加精确,然后在最终结果上加 1。Math.expm1()
能够实现这个不同的算法(m1
表示 “减 1”)。例如,清单 6 给出的 cosh 函数根据?x
的大小在两个算法之间进行切换:
这个例子有些呆板,因为在?Math.exp()
与?Math.expm1() + 1
之间的差别很明显的情况下,常常使用?ex,而不是?e-x。不过,Math.expm1()
在带有多种利率的金融计算中是非常实用的,比如短期国库券的日利率。
Math.log1p()
与?Math.expm1()
刚好相反,就像?Math.log()
与?Math.exp()
的关系一样。它计算 1 的对数和参数(1p
表示 “加 1”)。在值接近 1 的数字中使用这个函数。例如,应该使用它计算?Math.log1p(0.0002)
,而不是?Math.log(1.0002)
。
现在举一个例子,假设您需要知道在日利率为 0.03 的情况下,需要多少天投资才能使 $1,000 增长到 $1,100。清单 7 完成了这个计算任务:
在这个例子中,1p
的含义是很容易理解的,因为在计算类似数据的一般公式中通常出现 1+r。换句话说,尽管投资方很希望获得初始投资成本的 (1+r)n,贷方通常将利率作为附加的百分比(+r部分)。实际上,以 3% 的利率贷款的投资者如果仅能取回投资成本的 3% 的话,那就非常糟糕了。
浮点数并不是实数。它们的数量是有限的。它们能够表示最大和最小的值。更值得注意的是,它们的精度虽然很高,但范围很窄,并且容易出现舍入错误。相反,浮点数和双精度数处理整数时获得的精度远比整型数和长型数差。您必须仔细考虑这些限制,尤其是在科研和工程应用方面,以生产出健壮、可靠的代码。对于财务应用程序(尤其是需要精确到最后一位的会计应用程序),处理浮点数和双精度数时也需要格外小心。
java.lang.Math
和?java.lang.StrictMath
类经过了精心设计,可以解决这些问题。适当地使用这些类及其包含的方法能够改善程序。本文特别展示了良好的浮点算法有多么巧妙!最好使用专家提供的算法,而不是自己独创算法。如果适合使用?java.lang.Math
和java.lang.StrictMath
中提供的方法,最好继续使用。它们通常是最佳的选择。
原文:http://www.ibm.com/developerworks/cn/java/j-math2.html
(编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!