摘要:在这篇由两部分组成的文章中,Elliotte Rusty Harold 与您一起探讨经典 java.lang.Math 类中的“新”功能。第 1 部分主要讨论比较单调的数学函数。第 2 部分将探讨专为操作浮点数而设计的函数。
有时候您会对一个类熟悉到忘记了它的存在。如果您能够写出 java.lang.Foo 的文档,那么 Eclipse 将帮助您自动完成所需的函数,您无需阅读它的 Javadoc.例如,我使用 java.lang.Math(一个我自认为非常了解的类)时就是这样,但令我吃惊的是,我最近偶然读到它的 Javadoc —— 这可能是我近五年来第一次读到,我发现这个类的大小几乎翻了一倍,包含 20 种我从来没听说过的新方法。看来我要对它另眼相看了。
Java™语言规范第 5 版向 java.lang.Math(以及它的姊妹版 java.lang.StrictMath)添加了 10 种新方法,Java 6 又添加了 10 种。在本文中,我重点讨论其中的比较单调的数学函数,如 log10 和 cosh.在第 2 部分,我将探讨专为操作浮点数(与抽象实数相反)而设计的函数。
抽象实数(如 π 或 0.2)与 Java double 之间的区别很明显。首先,数的理想状态是具有无限的精度,而 Java 表示法把数限制为固定位数。在处理非常大和非常小的数时,这点很重要。例如,2,000,000,001(二十亿零一)可以精确表示为一个 int,而不是一个 float.最接近的浮点数表示形式是 2.0E9 — 即两亿。使用 double 数会更好,因为它们的位数更多(这是应该总是使用 double 数而不是 float 数的理由之一);但它们的精度仍然受到一定限制。
计算机算法(Java 语言和其他语言的算法)的第二个限制是它基于二进制而不是十进制。1/5 和 7/50 之类的分数可用十进制精确表示(分别是 0.2 和 0.14),但用二进制表示时,就会出现重复的分数。如同 1/3 在用十进制表示时,就会变为 0.3333333……以 10 为基数,任何分母仅包含质数因子 5 和 2 的分数都可以精确表示。以 2 为基数,则只有分母是 2 的乘方的分数才可以精确表示:1/2、1/4、1/8、1/16 等。
这种不精确性是迫切需要一个 math 类的最主要的原因之一。当然,您可以只使用标准的 + 和 * 运算符以及一个简单的循环来定义三角函数和其他使用泰勒级数展开式的函数,如清单 1 所示:
清单 1. 使用泰勒级数计算正弦
public class SineTaylor {
public static void main(String[] args) {
for (double angle = 0; angle <= 4*Math.PI; angle += Math.PI/8) {
System.out.println(degrees(angle) + "\t" + taylorSeriesSine(angle)
+ "\t" + Math.sin(angle));
}
}
public static double degrees(double radians) {
return 180 * radians/ Math.PI;
}
public static double taylorSeriesSine(double radians) {
double sine = 0;
int sign = 1;
for (int i = 1; i < 40; i+=2) {
sine += Math.pow(radians, i) * sign / factorial(i);
sign *= -1;
}
return sine;
}
private static double factorial(int i) {
double result = 1;
for (int j = 2; j <= i; j++) {
result *= j;
}
return result;
}
}
开始运行得不错,只有一点小的误差,如果存在误差的话,也只是最后一位小数不同:
0.0 0.0 0.0
22.5 0.3826834323650897 0.3826834323650898
45.0 0.7071067811865475 0.7071067811865475
67.5 0.923879532511287 0.9238795325112867
90.0 1.0000000000000002 1.0
但是,随着角度的增加,误差开始变大,这种简单的方法就不是很适用了:
630.0000000000003 -1.0000001371557132 -1.0
652.5000000000005 -0.9238801080153761 -0.9238795325112841
675.0000000000005 -0.7071090807463408 -0.7071067811865422
697.5000000000006 -0.3826922100671368 -0.3826834323650824
这里使用泰勒级数得到的结果实际上比我想像的要精确。但是,随着角度增加到 360 度、720 度(4 pi 弧度)以及更大时,泰勒级数就逐渐需要更多条件来进行准确计算。java.lang.Math 使用的更加完善的算法就避免了这一点。
泰勒级数的效率也无法与现代桌面芯片的内置正弦函数相比。要准确快速地计算正弦函数和其他函数,需要非常仔细的算法,专门用于避免无意地将小的误差变成大的错误。这些算法一般内置在硬件中以更快地执行。例如,几乎每个在最近 10 年内组装的 X86 芯片都具有正弦和余弦函的硬件实现,X86 VM 只需调用即可,不用基于较原始的运算缓慢地计算它们。HotSpot 利用这些指令显著加速了三角函数的运算。