程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java Math 類中的新功能,第 1 部分: 實數

Java Math 類中的新功能,第 1 部分: 實數

編輯:關於JAVA

有時候您會對一個類熟悉到忘記了它的存在。如果您能夠寫出 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 利用這些指令顯著加速了三角函數的運算。

直角三角形和歐幾裡德范數

每個高中學生都學過勾股定理:在直角三角形中,斜邊邊長的平方等於兩條直角邊邊長平 方之和。即 c 2 = a 2 + b 2

學習過大學物理和高等數學的同學會發現,這個等式會在很多地方出現,不只是在直角三 角形中。例如,R 2 的平方、二維向量的長度、三角不等式等都存在勾股定理。(事實上, 這些只是看待同一件事情的不同方式。重點在於勾股定理比看上去要重要得多)。

Java 5 添加了 Math.hypot 函數來精確執行這種計算,這也是庫很有用的一個出色的實 例證明。原始的簡單方法如下:

public static double hypot(double x, double y){
  return x*x + y*y;
}

實際代碼更復雜一些,如清單 2 所示。首先應注意的一點是,這是以本機 C 代碼編寫的 ,以使性能最大化。要注意的第二點是,它盡力使本計算中出現的錯誤最少。事實上,應根 據 x 和 y 的相對大小選擇不同的算法。

清單 2. 實現 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 <- |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;
}

實際上,是使用這種特定函數,還是幾個其他類似函數中的一個取決於平台上的 JVM 細 節。不過,這種代碼很有可能在 Sun 的標准 JDK 中調用。(其他 JDK 實現可以在必要時改 進它。)

這段代碼(以及 Sun Java 開發庫中的大多數其他本機數學代碼)來自 Sun 約 15 年前 編寫的開源 fdlibm 庫。該庫用於精確實現 IEE754 浮點數,能進行非常准確的計算,不過 會犧牲一些性能。

以 10 為底的對數

對數說明一個底數的幾次冪等於一個給定的值。也就是說,它是 Math.pow() 函數的反函 數。以 10 為底的對數一般出現在工程應用程序中。以 e為底的對數(自然對數)出現在復 合計算以及大量科學和數學應用程序中。以 2 為底的對數一般出現在算法分析中。

從 Java 1.0 開始,Math 類有了一個自然對數。也就是給定一個參數 x,該自然對數返 回 e 的幾次冪等於給定的值 x。遺憾的是,Java 語言的(以及 C 、Fortran 和 Basic 的 )自然對數函數錯誤命名為 log()。在我讀的每本數學教材中,log 都是以 10 為底的對數 ,而 ln 是以 e 為底的對數,lg 是以 2 為底的對數。現在已經來不及修復這個問題了,不 過 Java 5 添加了一個 log10() 函數,它是以 10 為底而不是以 e 為底的對數。

清單 3 是一個簡單程序,它輸出整數 1 到 100 的以 2、10 和 e 為底的對數:

清單 3. 1 到 100 的各種底數的對數

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 行結果:

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() 能正常終止對數函數執行:0 或任何負數的對數返回 NaN。

立方根

我不敢說我的生活中曾經需要過立方根,我也不是每天都要使用代數和幾何的少數人士之 一,更別提偶然涉足微積分、微分方程,甚至抽象代數。因此,下面這個函數對我毫無用處 。盡管如此,如果意外需要計算立方根,現在就可以了 — 使用自 Java 5 開始引入的 Math.cbrt() 方法。清單 4 通過計算 -5 到 5 之間的整數的立方根進行了演示:

清單 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));
     }
   }
}

下面是結果:

-1.709975946676697
-1.5874010519681996
-1.4422495703074083
-1.2599210498948732
-1.0
0.0
1.0
1.2599210498948732
1.4422495703074083
1.5874010519681996
1.709975946676697

結果顯示,與平方根相比,立方根擁有一個不錯的特性:每個實數只有一個實立方根。這 個函數只在其參數為 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 - e-i x)/2,cos(x) 可以寫成 (ei x + e-i x)/2,從這些公式中刪除虛數單位後即可得到雙曲正弦和雙曲余弦,即 sinh(x) = (e x - e -x)/2,cosh(x) = (e x + e -x)/2。

Java 5 添加了所有這三個函數:Math.cosh()、Math.sinh() 和 Math.tanh()。還沒有包 含反雙曲三角函數 — 反雙曲余弦、反雙曲正弦和反雙曲正切。

實際上,cosh(z) 的結果相當於一根吊繩兩端相連後得到的形狀,即懸鏈線。清單 5 是 一個簡單的程序,它使用 Math.cosh 函數繪制一條懸鏈線:

清單 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. 笛卡爾平面中的一條懸鏈曲線

雙曲正弦、雙曲余弦和雙曲正切函數也會以常見或特殊形式出現在各種計算中。

符號

Math.signum 函數將正數轉換為 1.0,將負數轉換為 -1.0,0 仍然是 0。 實際上,它只 是提取一個數的符號。在實現 Comparable 接口時,這很有用。

一個 float 和一個 double 版本可用來維護這種類型 。這個函數的用途很明顯,即處理 浮點運算、NaN 以及正 0 和負 0 的特殊情況。NaN 也被當作 0,正 0 和負 0 應該返回正 0 和 負 0。例如,假設如清單 6 那樣用簡單的原始方法實現這個函數:

清單 6. 存在問題的 Math.signum 實現

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 轉換為正 0。(負 0 可能不好理解,但它確實是 IEEE 754 規范的必要組成部分)。其次,它會認為 NaN 是正的。實際實現如清單 7 所示,它更 加復雜,而且會仔細處理這些特殊情況:

清單 7. 實際的、正確的 Math.signum 實現

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)));
}

事半功倍

最有效的代碼是從您未編寫過的代碼。不要做專家們已經做過的事情。使用 java.lang.Math 函數(新的和舊的)的代碼將更快、更有效,而且比您自己編寫的任何代碼 都准確。所以請使用這些函數。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved