java中怎样定义实数_Java Math 类中的新功能,第 1 部分: 实数
在這篇由兩部分組成的文章中,Elliotte Rusty Harold 與您一起探討經(jīng)典 java.lang.Math 類中的“新”功能。第 1 部分主要討論比較單調(diào)的數(shù)學函數(shù)。第 2 部分將探討專為操作浮點數(shù)而設(shè)計的函數(shù)。
有時候您會對一個類熟悉到忘記了它的存在。如果您能夠?qū)懗?java.lang.Foo 的文檔,那么 Eclipse 將幫助您自動完成所需的函數(shù),您無需閱讀它的 Javadoc。例如,我使用 java.lang.Math(一個我自認為非常了解的類)時就是這樣,但令我吃驚的是,我最近偶然讀到它的 Javadoc —— 這可能是我近五年來第一次讀到,我發(fā)現(xiàn)這個類的大小幾乎翻了一倍,包含 20 種我從來沒聽說過的新方法。看來我要對它另眼相看了。
Java? 語言規(guī)范第 5 版向 java.lang.Math(以及它的姊妹版 java.lang.StrictMath)添加了 10 種新方法,Java 6 又添加了 10 種。在本文中,我重點討論其中的比較單調(diào)的數(shù)學函數(shù),如 log10 和 cosh。在第 2 部分,我將探討專為操作浮點數(shù)(與抽象實數(shù)相反)而設(shè)計的函數(shù)。
抽象實數(shù)(如 π 或 0.2)與 Java double 之間的區(qū)別很明顯。首先,數(shù)的理想狀態(tài)是具有無限的精度,而 Java 表示法把數(shù)限制為固定位數(shù)。在處理非常大和非常小的數(shù)時,這點很重要。例如,2,000,000,001(二十億零一)可以精確表示為一個 int,而不是一個 float。最接近的浮點數(shù)表示形式是 2.0E9 — 即兩億。使用 double 數(shù)會更好,因為它們的位數(shù)更多(這是應(yīng)該總是使用 double 數(shù)而不是 float 數(shù)的理由之一);但它們的精度仍然受到一定限制。
計算機算法(Java 語言和其他語言的算法)的第二個限制是它基于二進制而不是十進制。1/5 和 7/50 之類的分數(shù)可用十進制精確表示(分別是
0.2 和 0.14),但用二進制表示時,就會出現(xiàn)重復的分數(shù)。如同 1/3 在用十進制表示時,就會變?yōu)?0.3333333……以 10
為基數(shù),任何分母僅包含質(zhì)數(shù)因子 5 和 2 的分數(shù)都可以精確表示。以 2 為基數(shù),則只有分母是 2
的乘方的分數(shù)才可以精確表示:1/2、1/4、1/8、1/16 等。
這種不精確性是迫切需要一個 math 類的最主要的原因之一。當然,您可以只使用標準的 + 和 * 運算符以及一個簡單的循環(huán)來定義三角函數(shù)和其他使用泰勒級數(shù)展開式的函數(shù),如清單 1 所示:
清單 1. 使用泰勒級數(shù)計算正弦
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;
}
}
開始運行得不錯,只有一點小的誤差,如果存在誤差的話,也只是最后一位小數(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
這里使用泰勒級數(shù)得到的結(jié)果實際上比我想像的要精確。但是,隨著角度增加到 360 度、720 度(4 pi 弧度)以及更大時,泰勒級數(shù)就逐漸需要更多條件來進行準確計算。java.lang.Math 使用的更加完善的算法就避免了這一點。
泰勒級數(shù)的效率也無法與現(xiàn)代桌面芯片的內(nèi)置正弦函數(shù)相比。要準確快速地計算正弦函數(shù)和其他函數(shù),需要非常仔細的算法,專門用于避
免無意地將小的誤差變成大的錯誤。這些算法一般內(nèi)置在硬件中以更快地執(zhí)行。例如,幾乎每個在最近 10 年內(nèi)組裝的 X86
芯片都具有正弦和余弦函的硬件實現(xiàn),X86 VM 只需調(diào)用即可,不用基于較原始的運算緩慢地計算它們。HotSpot
利用這些指令顯著加速了三角函數(shù)的運算。
每個高中學生都學過勾股定理:在直角三角形中,斜邊邊長的平方等于兩條直角邊邊長平方之和。即 c
2 =a
2 + b
2
學習過大學物理和高等數(shù)學的同學會發(fā)現(xiàn),這個等式會在很多地方出現(xiàn),不只是在直角三角形中。例如,R
2 的平方、二維向量的長度、三角不等式等都存在勾股定理。(事實上,這些只是看待同一件事情的不同方式。重點在于勾股定理比看上去要重要得多)。
Java 5 添加了 Math.hypot 函數(shù)來精確執(zhí)行這種計算,這也是庫很有用的一個出色的實例證明。原始的簡單方法如下:
public static double hypot(double x, double y){
return x*x + y*y;
}
實際代碼更復雜一些,如清單 2 所示。首先應(yīng)注意的一點是,這是以本機 C 代碼編寫的,以使性能最大化。要注意的第二點是,它盡力使本計算中出現(xiàn)的錯誤最少。事實上,應(yīng)根據(jù) x 和 y 的相對大小選擇不同的算法。
清單 2. 實現(xiàn) Math.hypot
的實際代碼/*
* ====================================================
* 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
__HI(b) = hb; /* 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ù)中的一個取決于平臺上的 JVM 細節(jié)。不過,這種代碼很有可能在 Sun 的標準 JDK 中調(diào)用。(其他 JDK 實現(xiàn)可以在必要時改進它。)
這段代碼(以及 Sun Java 開發(fā)庫中的大多數(shù)其他本機數(shù)學代碼)來自 Sun 約 15 年前編寫的開源 fdlibm 庫。該庫用于精確實現(xiàn) IEE754 浮點數(shù),能進行非常準確的計算,不過會犧牲一些性能。
對數(shù)說明一個底數(shù)的幾次冪等于一個給定的值。也就是說,它是 Math.pow() 函數(shù)的反函數(shù)。以 10 為底的對數(shù)一般出現(xiàn)在工程應(yīng)用程序中。以 e為底的對數(shù)(自然對數(shù))出現(xiàn)在復合計算以及大量科學和數(shù)學應(yīng)用程序中。以 2 為底的對數(shù)一般出現(xiàn)在算法分析中。
從 Java 1.0 開始,Math 類有了一個自然對數(shù)。也就是給定一個參數(shù) x,該自然對數(shù)返回 e 的幾次冪等于給定的值 x。遺憾的是,Java 語言的(以及 C 、Fortran 和 Basic 的)自然對數(shù)函數(shù)錯誤命名為 log()。在我讀的每本數(shù)學教材中,log 都是以 10 為底的對數(shù),而 ln 是以 e 為底的對數(shù),lg 是以 2 為底的對數(shù)。現(xiàn)在已經(jīng)來不及修復這個問題了,不過 Java 5 添加了一個 log10() 函數(shù),它是以 10 為底而不是以 e 為底的對數(shù)。
清單 3 是一個簡單程序,它輸出整數(shù) 1 到 100 的以 2、10 和 e 為底的對數(shù):
清單 3. 1 到 100 的各種底數(shù)的對數(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() 能正常終止對數(shù)函數(shù)執(zhí)行:0 或任何負數(shù)的對數(shù)返回 NaN。
我不敢說我的生活中曾經(jīng)需要過立方根,我也不是每天都要使用代數(shù)和幾何的少數(shù)人士之一,更別提偶然涉足微積分、微分方程,甚至抽象代數(shù)。因此,下面這個函數(shù)對我毫無用處。盡管如此,如果意外需要計算立方根,現(xiàn)在就可以了 — 使用自 Java 5 開始引入的 Math.cbrt() 方法。清單 4 通過計算 -5 到 5 之間的整數(shù)的立方根進行了演示:
清單 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é)果顯示,與平方根相比,立方根擁有一個不錯的特性:每個實數(shù)只有一個實立方根。這個函數(shù)只在其參數(shù)為 NaN 時才返回 NaN。
雙曲三角函數(shù)就是對曲線應(yīng)用三角函數(shù),也就是說,想象將這些點放在笛卡爾平面上來得到 t 的所有可能值:
x = r cos(t)
y = r sin(t)
您會得到以 r 為半徑的曲線。相反,假設(shè)改用雙曲正弦和雙曲余弦,如下所示:
x = r cosh(t)
y = r sinh(t)
則會得到一個正交雙曲線,原點與它最接近的點之間的距離是 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 添加了所有這三個函數(shù):Math.cosh()、Math.sinh() 和 Math.tanh()。還沒有包含反雙曲三角函數(shù) — 反雙曲余弦、反雙曲正弦和反雙曲正切。
實際上,cosh(z) 的結(jié)果相當于一根吊繩兩端相連后得到的形狀,即懸鏈線。清單 5 是一個簡單的程序,它使用 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ù)也會以常見或特殊形式出現(xiàn)在各種計算中。
Math.signum 函數(shù)將正數(shù)轉(zhuǎn)換為 1.0,將負數(shù)轉(zhuǎn)換為 -1.0,0 仍然是 0。 實際上,它只是提取一個數(shù)的符號。在實現(xiàn) Comparable 接口時,這很有用。
一個 float 和一個 double 版本可用來維護這種類型 。這個函數(shù)的用途很明顯,即處理浮點運算、NaN 以及正 0 和負 0 的特殊情況。NaN 也被當作 0,正 0 和負 0 應(yīng)該返回正 0 和 負 0。例如,假設(shè)如清單 6 那樣用簡單的原始方法實現(xiàn)這個函數(shù):
清單 6. 存在問題的 Math.signum 實現(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;
}
首先,這個方法會將所有負 0 轉(zhuǎn)換為正 0。(負 0 可能不好理解,但它確實是 IEEE 754 規(guī)范的必要組成部分)。其次,它會認為 NaN 是正的。實際實現(xiàn)如清單 7 所示,它更加復雜,而且會仔細處理這些特殊情況:
清單 7. 實際的、正確的 Math.signum 實現(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ù)(新的和舊的)的代碼將更快、更有效,而且比您自己編寫的任何代碼都準確。所以請使用這些函數(shù)。
學習
類型、值和變量:Java 語言規(guī)范的第 4 章討論了浮點運算。
二進制浮點運算的 IEEE 標準:IEEE 754 標準定義了大多數(shù)現(xiàn)代處理器和語言(包括 Java 語言)中的浮點運算。
java.lang.Math:提供本文所討論函數(shù)的類的 Javadoc。
Bug 5005861:不滿足的用戶要求 JDK 中包含更快的三角函數(shù)。
懸鏈線:Wikipedia 說明了懸鏈線的歷史及其背后的數(shù)學理論。
獲得產(chǎn)品和技術(shù)
fdlibm:一個適用于支持 IEEE 754 浮點數(shù)的機器的 C math 庫,可在 Netlib 數(shù)學軟件庫中找到。
OpenJDK:查看此開源 Java SE 實現(xiàn)中 math 類的源代碼。
Elliotte Rusty Harold 出生在新奧爾良,現(xiàn)在他還定期回老家喝一碗秋葵湯。他與他的妻子 Beth、寵物貓 Charm(以 quark 命名)和 Marjorie(以他岳母的名字命名)住在 Irvine 附近的大學城中心。他的 Cafe au Lait Web 站點已成為 Internet 上最流行的獨立 Java 站點之一,而且其姊妹站點 Cafe con Leche 已經(jīng)是最流行的 XML 站點之一。他最近的著作是 Refactoring HTML。
總結(jié)
以上是生活随笔為你收集整理的java中怎样定义实数_Java Math 类中的新功能,第 1 部分: 实数的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 8.0 慢查询_MySQL慢
- 下一篇: 2017 9月java答案_2017年9