摘要在这篇由两部分组成的文章中Elliotte Rusty Harold 与您一起探讨经典 javalangMath 类中的新功能第 部分主要讨论比较单调的数学函数第 部分将探讨专为操作浮点数而设计的函数
有时候您会对一个类熟悉到忘记了它的存在如果您能够写出 javalangFoo 的文档那么 Eclipse 将帮助您自动完成所需的函数您无需阅读它的 Javadoc例如我使用 javalangMath(一个我自认为非常了解的类)时就是这样但令我吃惊的是我最近偶然读到它的 Javadoc —— 这可能是我近五年来第一次读到我发现这个类的大小几乎翻了一倍包含 种我从来没听说过的新方法看来我要对它另眼相看了
Java;语言规范第 版向 javalangMath(以及它的姊妹版 javalangStrictMath)添加了 种新方法Java 又添加了 种在本文中我重点讨论其中的比较单调的数学函数如 log 和 cosh在第 部分我将探讨专为操作浮点数(与抽象实数相反)而设计的函数
抽象实数(如 π 或 )与 Java double 之间的区别很明显首先数的理想状态是具有无限的精度而 Java 表示法把数限制为固定位数在处理非常大和非常小的数时这点很重要例如(二十亿零一)可以精确表示为一个 int而不是一个 float最接近的浮点数表示形式是 E — 即两亿使用 double 数会更好因为它们的位数更多(这是应该总是使用 double 数而不是 float 数的理由之一)但它们的精度仍然受到一定限制
计算机算法(Java 语言和其他语言的算法)的第二个限制是它基于二进制而不是十进制/ 和 / 之类的分数可用十进制精确表示(分别是 和 )但用二进制表示时就会出现重复的分数如同 / 在用十进制表示时就会变为 ……以 为基数任何分母仅包含质数因子 和 的分数都可以精确表示以 为基数则只有分母是 的乘方的分数才可以精确表示//// 等
这种不精确性是迫切需要一个 math 类的最主要的原因之一当然您可以只使用标准的 + 和 * 运算符以及一个简单的循环来定义三角函数和其他使用泰勒级数展开式的函数如清单 所示
清单 使用泰勒级数计算正弦
public class SineTaylor {
public static void main(String[] args) {
for (double angle = ; angle <= *MathPI; angle += MathPI/) {
Systemoutprintln(degrees(angle) + \t + taylorSeriesSine(angle)
+ \t + Mathsin(angle));
}
}
public static double degrees(double radians) {
return * radians/ MathPI;
}
public static double taylorSeriesSine(double radians) {
double sine = ;
int sign = ;
for (int i = ; i < ; i+=) {
sine += Mathpow(radians i) * sign / factorial(i);
sign *= ;
}
return sine;
}
private static double factorial(int i) {
double result = ;
for (int j = ; j <= i; j++) {
result *= j;
}
return result;
}
}
开始运行得不错只有一点小的误差如果存在误差的话也只是最后一位小数不同
但是随着角度的增加误差开始变大这种简单的方法就不是很适用了
这里使用泰勒级数得到的结果实际上比我想像的要精确但是随着角度增加到 度 度( pi 弧度)以及更大时泰勒级数就逐渐需要更多条件来进行准确计算javalangMath 使用的更加完善的算法就避免了这一点
泰勒级数的效率也无法与现代桌面芯片的内置正弦函数相比要准确快速地计算正弦函数和其他函数需要非常仔细的算法专门用于避免无意地将小的误差变成大的错误这些算法一般内置在硬件中以更快地执行例如几乎每个在最近 年内组装的 X 芯片都具有正弦和余弦函的硬件实现X VM 只需调用即可不用基于较原始的运算缓慢地计算它们HotSpot 利用这些指令显着加速了三角函数的运算
直角三角形和欧几里德范数
每个高中学生都学过勾股定理在直角三角形中斜边边长的平方等于两条直角边边长平方之和即 c = a + b
学习过大学物理和高等数学的同学会发现这个等式会在很多地方出现不只是在直角三角形中例如R 的平方二维向量的长度三角不等式等都存在勾股定理(事实上这些只是看待同一件事情的不同方式重点在于勾股定理比看上去要重要得多)
Java 添加了 Mathhypot 函数来精确执行这种计算这也是库很有用的一个出色的实例证明原始的简单方法如下
public static double hypot(double x double y){
return x*x + y*y;
}
实际代码更复杂一些如清单 所示首先应注意的一点是这是以本机 C 代码编写的以使性能最大化要注意的第二点是它尽力使本计算中出现的错误最少事实上应根据 x 和 y 的相对大小选择不同的算法
清单 实现 Mathhypot
的实际代码/*
* ====================================================
* Copyright (C) by Sun Microsystems Inc All rights reserved
*
* Developed at SunSoft a Sun Microsystems Inc business
* Permission to use copy modify and distribute this
* software is freely granted provided that this notice
* is preserved
* ====================================================
*/
#include fdlibmh
#ifdef __STDC__
double __ieee_hypot(double x double y)
#else
double __ieee_hypot(xy)
double x y;
#endif
{
double a=xb=yttyyw;
int jkhahb;
ha = __HI(x)&xfffffff; /* high word of x */
hb = __HI(y)&xfffffff; /* high word of y */
if(hb > ha) {a=y;b=x;j=ha; ha=hb;hb=j;} else {a=x;b=y;}
__HI(a) = ha; /* a < |a| */
__HI(b) = hb; /* b < |b| */
if((hahb)>xc) {return a+b;} /* x/y > ** */
k=;
if(ha > xf) { /* a>** */
if(ha >= xff) { /* Inf or NaN */
w = a+b; /* for sNaN */
if(((ha&xfffff)|__LO(a))==) w = a;
if(((hb^xff)|__LO(b))==) w = b;
return w;
}
/* scale a and b by ** */
ha = x; hb = x; k += ;
__HI(a) = ha;
__HI(b) = hb;
}
if(hb < xb) { /* b < ** */
if(hb <= xfffff) { /* subnormal b or */
if((hb|(__LO(b)))==) return a;
t=;
__HI(t) = xfd; /* t=^ */
b *= t;
a *= t;
k = ;
} else { /* scale a and b by ^ */
ha += x; /* a *= ^ */
hb += x; /* b *= ^ */
k = ;
__HI(a) = ha;
__HI(b) = hb;
}
}
/* medium size a and b */
w = ab;
if (w>b) {
t = ;
__HI(t) = ha;
t = at;
w = sqrt(t*t(b*(b)t*(a+t)));
} else {
a = a+a;
y = ;
__HI(y) = hb;
y = b y;
t = ;
__HI(t) = ha+x;
t = a t;
w = sqrt(t*y(w*(w)(t*y+t*b)));
}
if(k!=) {
t = ;
__HI(t) += (k<<);
return t*w;
} else return w;
}
实际上是使用这种特定函数还是几个其他类似函数中的一个取决于平台上的 JVM 细节不过这种代码很有可能在 Sun 的标准 JDK 中调用(其他 JDK 实现可以在必要时改进它)
这段代码(以及 Sun Java 开发库中的大多数其他本机数学代码)来自 Sun 约 年前编写的开源 fdlibm 库该库用于精确实现 IEE 浮点数能进行非常准确的计算不过会牺牲一些性能
以为底的对数
对数说明一个底数的几次幂等于一个给定的值也就是说它是 Mathpow() 函数的反函数以 为底的对数一般出现在工程应用程序中以 e为底的对数(自然对数)出现在复合计算以及大量科学和数学应用程序中以 为底的对数一般出现在算法分析中
从 Java 开始Math 类有了一个自然对数也就是给定一个参数 x该自然对数返回 e 的几次幂等于给定的值 x遗憾的是Java 语言的(以及 C Fortran 和 Basic 的)自然对数函数错误命名为 log()在我读的每本数学教材中log 都是以 为底的对数而 ln 是以 e 为底的对数lg 是以 为底的对数现在已经来不及修复这个问题了不过 Java 添加了一个 log() 函数它是以 为底而不是以 e 为底的对数
清单 是一个简单程序它输出整数 到 的以 和 e 为底的对数
清单 到 的各种底数的对数
public static void main(String[] args) {
for (int i = ; i <= ; i++) {
Systemoutprintln(i + \t +
Mathlog(i) + \t +
Mathlog(i) + \t +
lg(i));
}
}
public static double lg(double x) {
return Mathlog(x)/Mathlog();
}
}
下面是前 行结果
Mathlog() 能正常终止对数函数执行 或任何负数的对数返回 NaN
立方根
我不敢说我的生活中曾经需要过立方根我也不是每天都要使用代数和几何的少数人士之一更别提偶然涉足微积分微分方程甚至抽象代数因此下面这个函数对我毫无用处尽管如此如果意外需要计算立方根现在就可以了 — 使用自 Java 开始引入的 Mathcbrt() 方法清单 通过计算 到 之间的整数的立方根进行了演示
清单 到 的立方根
public class CubeRoots {
public static void main(String[] args) {
for (int i = ; i <= ; i++) {
Systemoutprintln(Mathcbrt(i));
}
}
}
下面是结果
结果显示与平方根相比立方根拥有一个不错的特性每个实数只有一个实立方根这个函数只在其参数为 NaN 时才返回 NaN
双曲三角函数
双曲三角函数就是对曲线应用三角函数也就是说想象将这些点放在笛卡尔平面上来得到 t 的所有可能值
x = r cos(t)
y = r sin(t)
您会得到以 r 为半径的曲线相反假设改用双曲正弦和双曲余弦如下所示
x = r cosh(t)
y = r sinh(t)
则会得到一个正交双曲线原点与它最接近的点之间的距离是 r
还可以这样思考其中 sin(x) 可以写成 (ei x ei x)/cos(x) 可以写成 (ei x + ei x)/从这些公式中删除虚数单位后即可得到双曲正弦和双曲余弦即 sinh(x) = (e x e x)/cosh(x) = (e x + e x)/
Java 添加了所有这三个函数sh()Mathsinh() 和 Mathtanh()还没有包含反双曲三角函数 — 反双曲余弦反双曲正弦和反双曲正切
实际上cosh(z) 的结果相当于一根吊绳两端相连后得到的形状即悬链线清单 是一个简单的程序它使用 sh 函数绘制一条悬链线
清单 使用 sh() 绘制悬链线
import javaawt*;
public class Catenary extends Frame {
private static final int WIDTH = ;
private static final int HEIGHT = ;
private static final double MIN_X = ;
private static final double MAX_X = ;
private static final double MAX_Y = ;
private Polygon catenary = new Polygon();
public Catenary(String title) {
super(title);
setSize(WIDTH HEIGHT);
for (double x = MIN_X; x <= MAX_X; x += ) {
double y = sh(x);
int scaledX = (int) (x * WIDTH/(MAX_X MIN_X) + WIDTH/);
int scaledY = (int) (y * HEIGHT/MAX_Y);
// in computer graphics y extends down rather than up as in
// Caretesian coordinates so we have to flip
scaledY = HEIGHT scaledY;
catenaryaddPoint(scaledX scaledY);
}
}
public static void main(String[] args) {
Frame f = new Catenary(Catenary);
fsetVisible(true);
}
public void paint(Graphics g) {
gdrawPolygon(catenary);
}
}
图 为绘制的曲线
图 笛卡尔平面中的一条悬链曲线
双曲正弦双曲余弦和双曲正切函数也会以常见或特殊形式出现在各种计算中
符号
Mathsignum 函数将正数转换为 将负数转换为 仍然是 实际上它只是提取一个数的符号在实现 Comparable 接口时这很有用
一个 float 和一个 double 版本可用来维护这种类型 这个函数的用途很明显即处理浮点运算NaN 以及正 和负 的特殊情况NaN 也被当作 正 和负 应该返回正 和 负 例如假设如清单 那样用简单的原始方法实现这个函数
清单 存在问题的 Mathsignum 实现
public static double signum(double x) {
if (x == ) return ;
else if (x < ) return ;
else return ;
}
首先这个方法会将所有负 转换为正 (负 可能不好理解但它确实是 IEEE 规范的必要组成部分)其次它会认为 NaN 是正的实际实现如清单 所示它更加复杂而且会仔细处理这些特殊情况
清单 实际的正确的 Mathsignum 实现
public static double signum(double d) {
return (d == || isNaN(d))?d:copySign( d);
}
public static double copySign(double magnitude double sign) {
return rawCopySign(magnitude (isNaN(sign)?d:sign));
}
public static double rawCopySign(double magnitude double sign) {
return DoublelongBitsToDouble((DoubledoubleToRawLongBits(sign) &
(DoubleConstsSIGN_BIT_MASK)) |
(DoubledoubleToRawLongBits(magnitude) &
(DoubleConstsEXP_BIT_MASK |
DoubleConstsSIGNIF_BIT_MASK)));
}
事半功倍
最有效的代码是从您未编写过的代码不要做专家们已经做过的事情使用 javalangMath 函数(新的和旧的)的代码将更快更有效而且比您自己编写的任何代码都准确所以请使用这些函数
参考资料
您可以参阅本文在 developerWorks 全球网站上的 英文原文
类型值和变量Java 语言规范的第 章讨论了浮点运算
二进制浮点运算的 IEEE 标准IEEE 标准定义了大多数现代处理器和语言(包括 Java 语言)中的浮点运算
javalangMath
提供本文所讨论函数的类的 Javadoc
Bug 不满足的用户要求 JDK 中包含更快的三角函数
关于作者
Elliotte Rusty Harold 出生在新奥尔良现在他还定期回老家喝一碗秋葵汤他与他的妻子 Beth宠物猫 Charm(以 quark 命名)和 Marjorie(以他岳母的名字命名)住在 Irvine 附近的大学城中心他的 Cafe au Lait Web 站点已成为 Internet 上最流行的独立 Java 站点之一而且其姊妹站点 Cafe con Leche 已经是最流行的 XML 站点之一他最近的着作是 Refactoring HTML