栏目导航
热点推荐
- 三十条有用的 Java 编程规则
- Java制作水印图片源码
- Java常见异常及可能的导致原因
- Java中的修饰词使用方法总结
- J2EE系统异常的处理准则
- Java中的异常、断言、日志解析(
- Java面试技巧:Java面试题集锦(
- 面向Java开发人员的Scala指南:
- Java程序员:一刻钟精通正则表达
- 网友经验分享:学好java开发的关
- 专家解答:创建表格与数据库进行
- Java远程访问Domino数据库
阅览排行
Java Math 类中的新功能(一): 实数
www.jz123.cn 2009-08-31 来源: IT专家网 责任编辑(袁袁) 我要投递新闻
有时候您会对一个类熟悉到忘记了它的存在。如果您能够写出 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 浮点数,能进行非常准确的计算,不过会牺牲一些性能。
上一篇:MyEclipse内存不足之JVM内存浅谈 下一篇:面向Java开发人员的Scala指南: 增强Scitter库