原創(chuàng)|其它|編輯:郝浩|2009-08-31 13:42:07.000|閱讀 755 次
概述:在這篇由兩部分組成的文章中,Elliotte Rusty Harold 與您一起探討經(jīng)典 java.lang.Math 類中的“新”功能。第 1 部分主要討論比較單調(diào)的數(shù)學(xué)函數(shù)。第 2 部分將探討專為操作浮點(diǎn)數(shù)而設(shè)計(jì)的函數(shù)。
# 界面/圖表報(bào)表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
在這篇由兩部分組成的文章中,Elliotte Rusty Harold 與您一起探討經(jīng)典 java.lang.Math 類中的“新”功能。第 1 部分主要討論比較單調(diào)的數(shù)學(xué)函數(shù)。第 2 部分將探討專為操作浮點(diǎn)數(shù)而設(shè)計(jì)的函數(shù)。
有時(shí)候您會(huì)對(duì)一個(gè)類熟悉到忘記了它的存在。如果您能夠?qū)懗?java.lang.Foo 的文檔,那么 Eclipse 將幫助您自動(dòng)完成所需的函數(shù),您無需閱讀它的 Javadoc。例如,我使用 java.lang.Math(一個(gè)我自認(rèn)為非常了解的類)時(shí)就是這樣,但令我吃驚的是,我最近偶然讀到它的 Javadoc —— 這可能是我近五年來第一次讀到,我發(fā)現(xiàn)這個(gè)類的大小幾乎翻了一倍,包含 20 種我從來沒聽說過的新方法。看來我要對(duì)它另眼相看了。
Java™ 語言規(guī)范第 5 版向 java.lang.Math(以及它的姊妹版 java.lang.StrictMath)添加了 10 種新方法,Java 6 又添加了 10 種。在本文中,我重點(diǎn)討論其中的比較單調(diào)的數(shù)學(xué)函數(shù),如 log10 和 cosh。在第 2 部分,我將探討專為操作浮點(diǎn)數(shù)(與抽象實(shí)數(shù)相反)而設(shè)計(jì)的函數(shù)。
抽象實(shí)數(shù)(如 π 或 0.2)與 Java double 之間的區(qū)別很明顯。首先,數(shù)的理想狀態(tài)是具有無限的精度,而 Java 表示法把數(shù)限制為固定位數(shù)。在處理非常大和非常小的數(shù)時(shí),這點(diǎn)很重要。例如,2,000,000,001(二十億零一)可以精確表示為一個(gè) int,而不是一個(gè) float。最接近的浮點(diǎn)數(shù)表示形式是 2.0E9 — 即兩億。使用 double 數(shù)會(huì)更好,因?yàn)樗鼈兊奈粩?shù)更多(這是應(yīng)該總是使用 double 數(shù)而不是 float 數(shù)的理由之一);但它們的精度仍然受到一定限制。
計(jì)算機(jī)算法(Java 語言和其他語言的算法)的第二個(gè)限制是它基于二進(jìn)制而不是十進(jìn)制。1/5 和 7/50 之類的分?jǐn)?shù)可用十進(jìn)制精確表示(分別是 0.2 和 0.14),但用二進(jìn)制表示時(shí),就會(huì)出現(xiàn)重復(fù)的分?jǐn)?shù)。如同 1/3 在用十進(jìn)制表示時(shí),就會(huì)變?yōu)?0.3333333……以 10 為基數(shù),任何分母僅包含質(zhì)數(shù)因子 5 和 2 的分?jǐn)?shù)都可以精確表示。以 2 為基數(shù),則只有分母是 2 的乘方的分?jǐn)?shù)才可以精確表示:1/2、1/4、1/8、1/16 等。
這種不精確性是迫切需要一個(gè) math 類的最主要的原因之一。當(dāng)然,您可以只使用標(biāo)準(zhǔn)的 + 和 * 運(yùn)算符以及一個(gè)簡單的循環(huán)來定義三角函數(shù)和其他使用泰勒級(jí)數(shù)展開式的函數(shù),如清單 1 所示:
清單 1. 使用泰勒級(jí)數(shù)計(jì)算正弦
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;
}
}
開始運(yùn)行得不錯(cuò),只有一點(diǎn)小的誤差,如果存在誤差的話,也只是最后一位小數(shù)不同:
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
這里使用泰勒級(jí)數(shù)得到的結(jié)果實(shí)際上比我想像的要精確。但是,隨著角度增加到 360 度、720 度(4 pi 弧度)以及更大時(shí),泰勒級(jí)數(shù)就逐漸需要更多條件來進(jìn)行準(zhǔn)確計(jì)算。java.lang.Math 使用的更加完善的算法就避免了這一點(diǎn)。
泰勒級(jí)數(shù)的效率也無法與現(xiàn)代桌面芯片的內(nèi)置正弦函數(shù)相比。要準(zhǔn)確快速地計(jì)算正弦函數(shù)和其他函數(shù),需要非常仔細(xì)的算法,專門用于避免無意地將小的誤差變成大的錯(cuò)誤。這些算法一般內(nèi)置在硬件中以更快地執(zhí)行。例如,幾乎每個(gè)在最近 10 年內(nèi)組裝的 X86 芯片都具有正弦和余弦函的硬件實(shí)現(xiàn),X86 VM 只需調(diào)用即可,不用基于較原始的運(yùn)算緩慢地計(jì)算它們。HotSpot 利用這些指令顯著加速了三角函數(shù)的運(yùn)算。
直角三角形和歐幾里德范數(shù)
每個(gè)高中學(xué)生都學(xué)過勾股定理:在直角三角形中,斜邊邊長的平方等于兩條直角邊邊長平方之和。即 c 2 = a 2 + b 2
學(xué)習(xí)過大學(xué)物理和高等數(shù)學(xué)的同學(xué)會(huì)發(fā)現(xiàn),這個(gè)等式會(huì)在很多地方出現(xiàn),不只是在直角三角形中。例如,R 2 的平方、二維向量的長度、三角不等式等都存在勾股定理。(事實(shí)上,這些只是看待同一件事情的不同方式。重點(diǎn)在于勾股定理比看上去要重要得多)。
Java 5 添加了 Math.hypot 函數(shù)來精確執(zhí)行這種計(jì)算,這也是庫很有用的一個(gè)出色的實(shí)例證明。原始的簡單方法如下:
public static double hypot(double x, double y){
return x*x + y*y;
}
實(shí)際代碼更復(fù)雜一些,如清單 2 所示。首先應(yīng)注意的一點(diǎn)是,這是以本機(jī) C 代碼編寫的,以使性能最大化。要注意的第二點(diǎn)是,它盡力使本計(jì)算中出現(xiàn)的錯(cuò)誤最少。事實(shí)上,應(yīng)根據(jù) x 和 y 的相對(duì)大小選擇不同的算法。
清單 2. 實(shí)現(xiàn) Math.hypot
的實(shí)際代碼/*
* ====================================================
* Copyright (C) 1993 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 "fdlibm.h"
#ifdef __STDC__
double __ieee754_hypot(double x, double y)
#else
double __ieee754_hypot(x,y)
double x, y;
#endif
{
double a=x,b=y,t1,t2,y1,y2,w;
int j,k,ha,hb;
ha = __HI(x)&0x7fffffff; /* high word of x */
hb = __HI(y)&0x7fffffff; /* 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((ha-hb)>0x3c00000) {return a+b;} /* x/y > 2**60 */
k=0;
if(ha > 0x5f300000) { /* a>2**500 */
if(ha >= 0x7ff00000) { /* Inf or NaN */
w = a+b; /* for sNaN */
if(((ha&0xfffff)|__LO(a))==0) w = a;
if(((hb^0x7ff00000)|__LO(b))==0) w = b;
return w;
}
/* scale a and b by 2**-600 */
ha -= 0x25800000; hb -= 0x25800000; k += 600;
__HI(a) = ha;
__HI(b) = hb;
}
if(hb < 0x20b00000) { /* b < 2**-500 */
if(hb <= 0x000fffff) { /* subnormal b or 0 */
if((hb|(__LO(b)))==0) return a;
t1=0;
__HI(t1) = 0x7fd00000; /* t1=2^1022 */
b *= t1;
a *= t1;
k -= 1022;
} else { /* scale a and b by 2^600 */
ha += 0x25800000; /* a *= 2^600 */
hb += 0x25800000; /* b *= 2^600 */
k -= 600;
__HI(a) = ha;
__HI(b) = hb;
}
}
/* medium size a and b */
w = a-b;
if (w>b) {
t1 = 0;
__HI(t1) = ha;
t2 = a-t1;
w = sqrt(t1*t1-(b*(-b)-t2*(a+t1)));
} else {
a = a+a;
y1 = 0;
__HI(y1) = hb;
y2 = b - y1;
t1 = 0;
__HI(t1) = ha+0x00100000;
t2 = a - t1;
w = sqrt(t1*y1-(w*(-w)-(t1*y2+t2*b)));
}
if(k!=0) {
t1 = 1.0;
__HI(t1) += (k<<20);
return t1*w;
} else return w;
}
實(shí)際上,是使用這種特定函數(shù),還是幾個(gè)其他類似函數(shù)中的一個(gè)取決于平臺(tái)上的 JVM 細(xì)節(jié)。不過,這種代碼很有可能在 Sun 的標(biāo)準(zhǔn) JDK 中調(diào)用。(其他 JDK 實(shí)現(xiàn)可以在必要時(shí)改進(jìn)它。)
這段代碼(以及 Sun Java 開發(fā)庫中的大多數(shù)其他本機(jī)數(shù)學(xué)代碼)來自 Sun 約 15 年前編寫的開源 fdlibm 庫。該庫用于精確實(shí)現(xiàn) IEE754 浮點(diǎn)數(shù),能進(jìn)行非常準(zhǔn)確的計(jì)算,不過會(huì)犧牲一些性能。
以 10 為底的對(duì)數(shù)
對(duì)數(shù)說明一個(gè)底數(shù)的幾次冪等于一個(gè)給定的值。也就是說,它是 Math.pow() 函數(shù)的反函數(shù)。以 10 為底的對(duì)數(shù)一般出現(xiàn)在工程應(yīng)用程序中。以 e為底的對(duì)數(shù)(自然對(duì)數(shù))出現(xiàn)在復(fù)合計(jì)算以及大量科學(xué)和數(shù)學(xué)應(yīng)用程序中。以 2 為底的對(duì)數(shù)一般出現(xiàn)在算法分析中。
從 Java 1.0 開始,Math 類有了一個(gè)自然對(duì)數(shù)。也就是給定一個(gè)參數(shù) x,該自然對(duì)數(shù)返回 e 的幾次冪等于給定的值 x。遺憾的是,Java 語言的(以及 C 、Fortran 和 Basic 的)自然對(duì)數(shù)函數(shù)錯(cuò)誤命名為 log()。在我讀的每本數(shù)學(xué)教材中,log 都是以 10 為底的對(duì)數(shù),而 ln 是以 e 為底的對(duì)數(shù),lg 是以 2 為底的對(duì)數(shù)。現(xiàn)在已經(jīng)來不及修復(fù)這個(gè)問題了,不過 Java 5 添加了一個(gè) log10() 函數(shù),它是以 10 為底而不是以 e 為底的對(duì)數(shù)。
清單 3 是一個(gè)簡單程序,它輸出整數(shù) 1 到 100 的以 2、10 和 e 為底的對(duì)數(shù):
清單 3. 1 到 100 的各種底數(shù)的對(duì)數(shù)
public class Logarithms {
public static void main(String[] args) {
for (int i = 1; i <= 100; i++) {
System.out.println(i + "\t" +
Math.log10(i) + "\t" +
Math.log(i) + "\t" +
lg(i));
}
}
public static double lg(double x) {
return Math.log(x)/Math.log(2.0);
}
}
下面是前 10 行結(jié)果:
1 0.0 0.0 0.0
2 0.3010299956639812 0.6931471805599453 1.0
3 0.47712125471966244 1.0986122886681096 1.584962500721156
4 0.6020599913279624 1.3862943611198906 2.0
5 0.6989700043360189 1.6094379124341003 2.321928094887362
6 0.7781512503836436 1.791759469228055 2.584962500721156
7 0.8450980400142568 1.9459101490553132 2.807354922057604
8 0.9030899869919435 2.0794415416798357 3.0
9 0.9542425094393249 2.1972245773362196 3.1699250014423126
10 1.0 2.302585092994046 3.3219280948873626
Math.log10() 能正常終止對(duì)數(shù)函數(shù)執(zhí)行:0 或任何負(fù)數(shù)的對(duì)數(shù)返回 NaN。
立方根
我不敢說我的生活中曾經(jīng)需要過立方根,我也不是每天都要使用代數(shù)和幾何的少數(shù)人士之一,更別提偶然涉足微積分、微分方程,甚至抽象代數(shù)。因此,下面這個(gè)函數(shù)對(duì)我毫無用處。盡管如此,如果意外需要計(jì)算立方根,現(xiàn)在就可以了 — 使用自 Java 5 開始引入的 Math.cbrt() 方法。清單 4 通過計(jì)算 -5 到 5 之間的整數(shù)的立方根進(jìn)行了演示:
清單 4. -5 到 5 的立方根
public class CubeRoots {
public static void main(String[] args) {
for (int i = -5; i <= 5; i++) {
System.out.println(Math.cbrt(i));
}
}
}
下面是結(jié)果:
-1.709975946676697
-1.5874010519681996
-1.4422495703074083
-1.2599210498948732
-1.0
0.0
1.0
1.2599210498948732
1.4422495703074083
1.5874010519681996
1.709975946676697
結(jié)果顯示,與平方根相比,立方根擁有一個(gè)不錯(cuò)的特性:每個(gè)實(shí)數(shù)只有一個(gè)實(shí)立方根。這個(gè)函數(shù)只在其參數(shù)為 NaN 時(shí)才返回 NaN。
雙曲三角函數(shù)
雙曲三角函數(shù)就是對(duì)曲線應(yīng)用三角函數(shù),也就是說,想象將這些點(diǎn)放在笛卡爾平面上來得到 t 的所有可能值:
x = r cos(t)
y = r sin(t)
您會(huì)得到以 r 為半徑的曲線。相反,假設(shè)改用雙曲正弦和雙曲余弦,如下所示:
x = r cosh(t)
y = r sinh(t)
則會(huì)得到一個(gè)正交雙曲線,原點(diǎn)與它最接近的點(diǎn)之間的距離是 r。
還可以這樣思考:其中 sin(x) 可以寫成 (ei x - e-i x)/2,cos(x) 可以寫成 (ei x + e-i x)/2,從這些公式中刪除虛數(shù)單位后即可得到雙曲正弦和雙曲余弦,即 sinh(x) = (e x - e -x)/2,cosh(x) = (e x + e -x)/2。
Java 5 添加了所有這三個(gè)函數(shù):Math.cosh()、Math.sinh() 和 Math.tanh()。還沒有包含反雙曲三角函數(shù) — 反雙曲余弦、反雙曲正弦和反雙曲正切。
實(shí)際上,cosh(z) 的結(jié)果相當(dāng)于一根吊繩兩端相連后得到的形狀,即懸鏈線。清單 5 是一個(gè)簡單的程序,它使用 Math.cosh 函數(shù)繪制一條懸鏈線:
清單 5. 使用 Math.cosh() 繪制懸鏈線
import java.awt.*;
public class Catenary extends Frame {
private static final int WIDTH = 200;
private static final int HEIGHT = 200;
private static final double MIN_X = -3.0;
private static final double MAX_X = 3.0;
private static final double MAX_Y = 8.0;
private Polygon catenary = new Polygon();
public Catenary(String title) {
super(title);
setSize(WIDTH, HEIGHT);
for (double x = MIN_X; x <= MAX_X; x += 0.1) {
double y = Math.cosh(x);
int scaledX = (int) (x * WIDTH/(MAX_X - MIN_X) + WIDTH/2.0);
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;
catenary.addPoint(scaledX, scaledY);
}
}
public static void main(String[] args) {
Frame f = new Catenary("Catenary");
f.setVisible(true);
}
public void paint(Graphics g) {
g.drawPolygon(catenary);
}
}
圖 1 為繪制的曲線:
圖 1. 笛卡爾平面中的一條懸鏈曲線
雙曲正弦、雙曲余弦和雙曲正切函數(shù)也會(huì)以常見或特殊形式出現(xiàn)在各種計(jì)算中。
符號(hào)
Math.signum 函數(shù)將正數(shù)轉(zhuǎn)換為 1.0,將負(fù)數(shù)轉(zhuǎn)換為 -1.0,0 仍然是 0。 實(shí)際上,它只是提取一個(gè)數(shù)的符號(hào)。在實(shí)現(xiàn) Comparable 接口時(shí),這很有用。
一個(gè) float 和一個(gè) double 版本可用來維護(hù)這種類型 。這個(gè)函數(shù)的用途很明顯,即處理浮點(diǎn)運(yùn)算、NaN 以及正 0 和負(fù) 0 的特殊情況。NaN 也被當(dāng)作 0,正 0 和負(fù) 0 應(yīng)該返回正 0 和 負(fù) 0。例如,假設(shè)如清單 6 那樣用簡單的原始方法實(shí)現(xiàn)這個(gè)函數(shù):
清單 6. 存在問題的 Math.signum 實(shí)現(xiàn)
public static double signum(double x) {
if (x == 0.0) return 0;
else if (x < 0.0) return -1.0;
else return 1.0;
}
首先,這個(gè)方法會(huì)將所有負(fù) 0 轉(zhuǎn)換為正 0。(負(fù) 0 可能不好理解,但它確實(shí)是 IEEE 754 規(guī)范的必要組成部分)。其次,它會(huì)認(rèn)為 NaN 是正的。實(shí)際實(shí)現(xiàn)如清單 7 所示,它更加復(fù)雜,而且會(huì)仔細(xì)處理這些特殊情況:
清單 7. 實(shí)際的、正確的 Math.signum 實(shí)現(xiàn)
public static double signum(double d) {
return (d == 0.0 || isNaN(d))?d:copySign(1.0, d);
}
public static double copySign(double magnitude, double sign) {
return rawCopySign(magnitude, (isNaN(sign)?1.0d:sign));
}
public static double rawCopySign(double magnitude, double sign) {
return Double.longBitsToDouble((Double.doubleToRawLongBits(sign) &
(DoubleConsts.SIGN_BIT_MASK)) |
(Double.doubleToRawLongBits(magnitude) &
(DoubleConsts.EXP_BIT_MASK |
DoubleConsts.SIGNIF_BIT_MASK)));
}
事半功倍
最有效的代碼是從您未編寫過的代碼。不要做專家們已經(jīng)做過的事情。使用 java.lang.Math 函數(shù)(新的和舊的)的代碼將更快、更有效,而且比您自己編寫的任何代碼都準(zhǔn)確。所以請(qǐng)使用這些函數(shù)。
本站文章除注明轉(zhuǎn)載外,均為本站原創(chuàng)或翻譯。歡迎任何形式的轉(zhuǎn)載,但請(qǐng)務(wù)必注明出處、不得修改原文相關(guān)鏈接,如果存在內(nèi)容上的異議請(qǐng)郵件反饋至chenjj@fc6vip.cn
文章轉(zhuǎn)載自:IT專家網(wǎng)