Numbers
的类进行审查。Numbers.java
package library.util;
/**
* This class provides a useful interface for easy (approximate) floating
* point equality checks. This is necessary for several other classes.
*
* @author Subhomoy Haldar (ambigram_maker)
* @version 1.0
*/
public class Numbers {
/**
* The tolerance value for comparing the {@code float} values.
*/
public static double FLOAT_TOLERANCE = 5E-8;
/**
* The tolerance value for comparing the {@code double} values.
*/
public static double DOUBLE_TOLERANCE = 5E-16;
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param bytes The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(byte... bytes) {
int length = bytes.length;
checkLength(length);
byte d = bytes[0];
for (int i = 1; i < length; i++) {
if (d != bytes[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param shorts The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(short... shorts) {
int length = shorts.length;
checkLength(length);
short d = shorts[0];
for (int i = 1; i < length; i++) {
if (d != shorts[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param ints The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(int... ints) {
int length = ints.length;
checkLength(length);
int d = ints[0];
for (int i = 1; i < length; i++) {
if (d != ints[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param longs The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(long... longs) {
int length = longs.length;
checkLength(length);
long d = longs[0];
for (int i = 1; i < length; i++) {
if (d != longs[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>exactly</i> equal.
*
* @param chars The arguments to check.
* @return {@code true} if the arguments are <i>exactly</i> equal.
*/
public static boolean areEqual(char... chars) {
int length = chars.length;
checkLength(length);
char d = chars[0];
for (int i = 1; i < length; i++) {
if (d != chars[i]) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param floats The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(float... floats) {
int length = floats.length;
checkLength(length);
float d = floats[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, floats[i])) {
return false;
}
}
return true;
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param doubles The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(double... doubles) {
int length = doubles.length;
checkLength(length);
double d = doubles[0];
for (int i = 1; i < length; i++) {
if (!areEqual(d, doubles[i])) {
return false;
}
}
return true;
}
private static void checkLength(int length) throws
IllegalArgumentException {
if (length < 2) {
throw new IllegalArgumentException
("At least two arguments required.");
}
}
private static boolean areEqual(float f1, float f2) {
// the corner cases first:
if (Float.isNaN(f1) && Float.isNaN(f2)) {
return true;
}
if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
return f1 == f2;
}
float abs;
if (f1 == f2 || (abs = Math.abs(f1 - f2)) <= FLOAT_TOLERANCE) {
return true;
}
// compare using the larger ulp
float ulp1 = Math.ulp(f1);
float ulp2 = Math.ulp(f2);
return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
}
private static boolean areEqual(double d1, double d2) {
// the corner cases first:
if (Double.isNaN(d1) && Double.isNaN(d2)) {
return true;
}
if (Double.isInfinite(d1) || Double.isInfinite(d2)) {
return d1 == d2;
}
double abs;
if (d1 == d2 || (abs = Math.abs(d1 - d2)) <= DOUBLE_TOLERANCE) {
return true;
}
// compare using the larger ulp
double ulp1 = Math.ulp(d1);
double ulp2 = Math.ulp(d2);
return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
}
}
我希望您尽全力并检查实现的缺点(尤其是
float
和double
方法)。Numbers.java
的原因(和用法)我建议使用以下方法:
boolean example = Numbers.areEqual(60, Math.toDegrees(Math.acos(0.5)));
(我认为)这很容易阅读和理解它的作用。
#1 楼
您关于平等的数学标准存在一些问题。要考虑的重要一点是,您的“平等”不是可传递的。在代码中,您使用的阈值为\ $ \ epsilon \ $(当值的类型为
double
时,在代码中使用\ $ \ epsilon = 5 \ times10 ^ {-16} \ $);如果\ $ | x-y | \ leq \ epsilon \ $,则声明\ $ x \ $和\ $ y \ $彼此相等。现在假设您有三个值\ $ a \ $,\ $ b \ $和\ $ c \ $,使得\ $ b = a + 0.9 \ epsilon \ $和\ $ c = a-0.9 \ epsilon \ $。在这种情况下,areEqual(a, b, c)
将返回true
,但是areEqual(b, a, c)
将返回false。这是令人惊讶的行为-areEqual()
的文档没有说结果取决于参数的顺序。要解决某些症状,您需要比较所有值对:
areEqual(a, b, c)
应返回仅当true
,areEqual(a, b)
和areEqual(a, c)
全部返回areEqual(b, c)
时,true
。一般而言,这是二次的:使用10个参数,您最终进行了45次比较。具有20个参数和190个比较...这可能会在计算上令人望而却步。尽管这可以避免结果取决于参数顺序的问题,但仍不会使关系传递:areEqual(a, b)
和areEqual(a, c)
可以返回true
,而areEqual(b, c)
可以返回false
。而且,使用差异通常不是近似的“正确”方法。如果\ $ x = 10 ^ {-30} \ $和\ $ y = 10 ^ {-36} \ $,则通常不应将它们视为相等,甚至近似,因为\ $ x \ $是一百万比\ $ y \ $大一倍-但您的
areEqual()
会声明它们彼此相等。您会遇到大数字的双重问题:当\ $ x \ $大时(double
值接近\ $ 2 ^ {53} \ $),\ $ x \ $和\ $ x + 1 \ $(例如)差等于\ $ 1 \ $(因此大于\ $ \ epsilon \ $),但实际上它们的最低有效位只是不同;您使用Math.ulp()
显式添加了一些代码来处理这种情况。在某些使用上下文中,差异才是最有意义的。但一般来说,比率是更合适的。在物理学中,值是度量,具有给定的精度,该精度或多或少是有效数字的数量。比率反映了这一概念。在数学上,这意味着如果\ $ |(x / y)-1 |,则声明\ $ x \ $和\ $ y \ $彼此近似相等。 \ leq \ epsilon \ $(还有\ $ |(y / x)-1 | \ leq \ epsilon \ $,使该关系至少对称)。这不能解决所有情况,您必须对非常接近\ $ 0 \ $的值做一些事情;但是该标准将是“近似相等”的更正确概念,它将平滑地上下扩展,并避免使用
Math.ulp()
进行繁琐的交易。评论
\ $ \ begingroup \ $
欢迎使用代码审查!这是一个无代码答案的非常好的示例,感谢您的贡献:-)
\ $ \ endgroup \ $
– Mathieu Guindon♦
2015年9月2日,下午1:06
\ $ \ begingroup \ $
第一次阅读您的答案时,我试图捍卫自己的实现。我第二次开始思考。我第三次因为自己是个白痴而受诅咒。我一定会接受您的建议。顺便说一句,公差可以由用户根据需要进行修改。因此,如果用户这样做:Numbers.DOUBLE_TOLERANCE = 1e-40 ;,则Numbers.areEqual(1e-30,1e-36)返回false。
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,下午6:06
\ $ \ begingroup \ $
“总的来说,这是二次方的”-寻找线性算法并不困难;首先处理像NaN和无穷大这样的边缘情况,然后返回max(a,b,c)-min(a,b,c)
–塔米尔
2015年9月2日,9:10
\ $ \ begingroup \ $
我还要指出,OP的实现将NaN等同于NaN-这违反了浮点标准。
\ $ \ endgroup \ $
–塔米尔
2015年9月2日,9:15
#2 楼
您真的需要一个新的实现进行浮点数比较吗?
java.math.BigDecimal
可能通过其compareTo(arg)方法对FP编号比较有用。areEqual(args...)
重载似乎过于重复。您可以考虑使用单一方法中的泛型来重新定义它们。一个示例,其中原始代码略有更改:public static <T extends Number> boolean areEqual(T... numbers) {
int length = numbers.length;
checkLength(length);
T d = numbers[0];
for (int i = 1; i < length; i++) {
if (!d.equals(numbers[i])) {
return false;
}
}
return true;
}
当然,在这种情况下,您将松开基元,因为它们将被装箱,但这将节省多行代码。
评论
\ $ \ begingroup \ $
嗨!欢迎使用代码审查!我可能建议您继续游览并阅读我们的帮助中心:-)。同时,通过正确设置列表格式,我使您的帖子更易于阅读。
\ $ \ endgroup \ $
– Ethan Bierlein
2015年9月1日于21:52
#3 楼
关于FLOAT_TOLERANCE
和DOUBLE_TOLERANCE
的另一个小问题(比数学更多的编程问题):如果
Numbers
的外部世界需要了解这些值,请将其定为最终值(避免有人对其进行修改并更改未来数字比较的正确性)否则,将它们设为
private
更新
public class Numbers {
/**
* The tolerance value for comparing the {@code float} values.
*/
public static final float FLOAT_TOLERANCE = 5E-8f;
/**
* The tolerance value for comparing the {@code double} values.
*/
public static final double DOUBLE_TOLERANCE = 5E-16;
...
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal. The default delta used for comparisons is {@value #FLOAT_TOLERANCE}.
*
* @param floats The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEquals(float... floats) {
return areEqual(FLOAT_TOLERANCE, floats);
}
/**
* Returns {@code true} if the arguments are <i>approximately</i> equal.
*
* @param tolerance The delta for comparisons.
* @param floats The arguments to check.
* @return {@code true} if the arguments are <i>approximately</i> equal.
*/
public static boolean areEqual(float tolerance, float... floats) {
int length = floats.length;
checkLength(length);
float d = floats[0];
for (int i = 1; i < length; i++) {
if (!areEqual_impl(tolerance, d, floats[i])) {
return false;
}
}
return true;
}
private static boolean areEqual_impl(float tolerance, float f1, float f2) {
// the corner cases first:
if (Float.isNaN(f1) && Float.isNaN(f2)) {
return true;
}
if (Float.isInfinite(f1) || Float.isInfinite(f2)) {
return f1 == f2;
}
float abs;
if (f1 == f2 || (abs = Math.abs(f1 - f2)) <= tolerance) {
return true;
}
// compare using the larger ulp
float ulp1 = Math.ulp(f1);
float ulp2 = Math.ulp(f2);
return abs <= (ulp1 > ulp2 ? ulp1 : ulp2);
}
//Same principle for double
}
评论
\ $ \ begingroup \ $
好吧,我打算为用户提供灵活性。就像我在Tom Leek的答案中评论的那样:“用户可以根据需要修改TOLERANCE。因此,如果用户这样做:Numbers.DOUBLE_TOLERANCE = 1e-40 ;,则Numbers.areEqual(1e-30,1e-36)返回假。”
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,14:37
\ $ \ begingroup \ $
@ambigram_maker我用满足您要求(灵活性)的解决方案更新了答案,并通过添加考虑公差的方法使其更加可靠(无副作用,更易于测试)。 FLOAT_TOLERANCE和DOUBLE_TOLERANCE被声明为公共静态最终值(请注意最终值),以避免任何用户对其进行修改。
\ $ \ endgroup \ $
–发现
2015年9月2日在18:12
\ $ \ begingroup \ $
好吧,我喜欢这个主意。但是有一个陷阱:它。不。编译。 :-/
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月4日在9:47
\ $ \ begingroup \ $
另外,为什么要对FLOAT_TOLERANCE使用double?
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月4日在9:49
\ $ \ begingroup \ $
@ambigram_maker,您是对的,我没有注意到您在提供的代码中将FLOAT_TOLERANCE声明为double。我将其更改为浮动。
\ $ \ endgroup \ $
–发现
2015年9月4日在9:55
#4 楼
对我来说,这是干净的代码。有很多重复的代码,但这是因为Java不支持基元的泛型。即使重复,也很容易阅读和理解。直截了当。
我认为
ckeckLength()
是此方法的错误名称。也许assertAtLeastTwoElements(array)
或更具描述性的内容会更好。 浮点数的equals方法似乎是正确的。希望您已经编写了一些测试用例。
但是我想的最大的事情是极端情况。
NaN对我来说是一个错误情况。在数学上,您不能说
1/0
等于2/0
,因为您实际上并不知道某个数字除以零是什么。这取决于用例,但是通常,如果出现以下情况,应用程序应该爆炸并显示大错误消息:我认为计算的结果是NaN。
无穷大也是...嗯...
如果数学成立,则数学相等:\ $ x = y \ Leftrightarrow | x-y | = 0 \ $。这就是你在做什么。对于浮点:\ $ x = y \ Leftrightarrow | x-y | <\ epsilon \ $。但是对于无穷大,它是\ $ | \ infty-\ infty | = \ infty \ $(对于NaN也是如此)。所以它不成立。
无限用例可能会有一个更大的用例,它可能是所需的行为,因此可能很好,但是应记录此行为。
最后,您会说:漂亮干净的代码。干得好!
评论
\ $ \ begingroup \ $
是否需要取模? 1-1 = 0等于| 1-1 | = 10
\ $ \ endgroup \ $
– Caridorc
2015年9月1日22:21在
\ $ \ begingroup \ $
您看到... 1/0导致ArithmeticException,但1.0 / 0返回Double.POSITIVE_INFINITY。给出Double.NaN是0.0 / 0.0。
\ $ \ endgroup \ $
–饥饿的蓝色开发者
2015年9月2日,下午4:10
评论
请注意,浮点算术不是不准确的。它只是不符合人们对实数的期望。您必须按照自己的意愿接受它,就像您接受(1/3)* 3不是1一样。@PeteBecker是的,但是所有程序都需要一种变通的方法来满足人们的期望吗?您是否愿意使用图书馆来简化生活?
不,您必须像使用整数算术一样调整期望值。问题是大多数人(合法地)不了解浮点算法的作用。 “几乎平等”试图通过假装事实并非如此来解决这种缺乏理解的情况。
我完全同意。但是,如果尝试说tan 90不是无限,我肯定会被踢出课堂。 ;-)
tan 90是无限(宽松地说),tanπ/ 2也是如此;这些是关于实数的陈述。 tan(3.14159 / 2)可能是一个很大的有限值;那是关于浮点运算的声明。