如何使用BigDecimal来提高此方法的准确性?

How do I use BigDecimal to increase the accuracy of this method?

提问人:Todd R 提问时间:5/11/2021 更新时间:8/22/2022 访问量:144

问:

我编写了以下简单的函数,用于计算整数倒数的反弧。我想知道如何使用 BigDecimal 而不是 double 来提高结果的准确性。我还考虑使用 BigInteger 来存储“术语”值除以的 xSquare 的不断增长的倍数。

我对如何在 BigDecimal 上执行计算的语法经验有限。我将如何修改此功能以使用它们?

/* 感谢 https://www.cygnus-software.com/misc/pidigits.htm 对一般计算方法的解释 归功于约翰·马钦(John Machin)。 */

public static double atanInvInt(int x) {
        // Returns the arc tangent of an inverse integer
        /* Terminates once the remaining amount reaches zero or the denominator reaches 2101.
        If the former happens, the accuracy should be determined by the number format used, such as double.
        If the latter happens, the result should be off by at most one from the correct nearest value
                    in the seventh decimal place, if allowed by the accuracy of the number format used.
                    This likely only happens if the integer is 1.
        */
        int xSquare = x*x;
        double result = ((double)1)/x;
        double term = ((double)1)/x;
        int divisor = 1;
        double midResult;
        while ((term > 0)) {
            term = term / xSquare;
            divisor += 2;
            midResult = result - term/divisor;
            term = term /xSquare;
            divisor += 2;
            result = midResult + term/divisor;
            if (divisor >= 2101) {
                return ((result + midResult) / 2);
            }
        }
        return result;
    }
java 精度 bigdecimal

评论

1赞 Henry Twist 5/11/2021
在这种情况下,您已经获得了答案,但是将来为什么不查阅教程呢?当那里有成千上万的文件时,要求某人为你写一个似乎很奇怪,更不用说堆积如山的官方和非官方文档了。
0赞 Todd R 5/12/2021
不幸的是,要充分表达我的答案,为什么看到代码“在运行”来执行我想做的特定事情是有帮助的,这需要比分配的 600 个字符多得多。我不知道是将其作为十条评论的系列发布还是作为答案发布更合适,但我可以描述出为什么看到此示例代码与阅读文档不同的许多原因。

答:

1赞 Armando Carballo 5/11/2021 #1

BigDecimal 提供了非常直观的包装方法来提供所有不同的操作。您可以拥有这样的任意精度,例如 99:

    public static void main(String[] args) {
        System.out.println(atanInvInt(5, 99));
        // 0.197395559849880758370049765194790293447585103787852101517688940241033969978243785732697828037288045
    }

    public static BigDecimal atanInvInt(int x, int scale) {
        BigDecimal one = new BigDecimal("1");
        BigDecimal two = new BigDecimal("2");
        BigDecimal xVal = new BigDecimal(x);
        BigDecimal xSquare = xVal.multiply(xVal);
        BigDecimal divisor = new BigDecimal(1);

        BigDecimal result = one.divide(xVal, scale, RoundingMode.FLOOR);
        BigDecimal term = one.divide(xVal, scale, RoundingMode.FLOOR);
        BigDecimal midResult;

        while (term.compareTo(new BigDecimal(0)) > 0) {
            term = term.divide(xSquare, scale, RoundingMode.FLOOR);
            divisor = divisor.add(two);
            midResult = result.subtract(term.divide(divisor, scale, RoundingMode.FLOOR));
            term = term.divide(xSquare, scale, RoundingMode.FLOOR);
            divisor = divisor.add(two);
            result = midResult.add(term.divide(divisor, scale, RoundingMode.FLOOR));

            if (divisor.compareTo(new BigDecimal(2101)) >= 0) {
                return result.add(midResult).divide(two, scale, RoundingMode.FLOOR);
            }
        }
        return result;
    }
0赞 Todd R 5/12/2021 #2

对于任何想知道为什么一开始提出这个问题是有益的:这是一个公平的问题。我已经写了一个相当长的答案。我相信写这个答案有助于我向自己阐明关于 BigDecimal 类的事情,这些事情现在我有了 Armando Carballo 的答案,比以前更直观,所以写它希望能有教育意义。我只能希望阅读它也会如此,尽管可能会以不同的方式阅读。

官方文档列出了方法,但没有解释如何像 Armando Carballo 的代码演示的那样使用它们。例如,虽然 BigDecimal.divide 方法的工作方式非常直观,但官方文档中没有任何内容说“要取两个数字的平均值,不仅应该为这两个数字使用 BigDecimal,而且还应该创建一个等于 2 的 BigDecimal,并将 BigDecimal.divide 方法应用于 BigDecimal.add 操作的结果,并将 2 BigDecimal 作为除数的输入。这是一件简单的事情,一旦你看到它就非常直观,但是如果你以前从未使用过面向对象的方法来执行算术的特定目的,那么当你第一次尝试弄清楚如何取平均值时,它可能会不那么直观。

再举一个例子,考虑这样的想法:要确定一个数字是否大于或等于另一个数字,而不是对两个数字使用布尔运算符,而是使用 compareTo 方法,该方法可以在一个数字上提供三个可能的输出,另一个数字作为输入,然后将布尔运算符应用于该方法的输出。一旦你看到它的实际效果并快速了解了 compareTo 方法的工作原理,这就非常有意义了,但当你盯着官方文档中 compareTo 方法的快速描述时,可能就不那么明显了,即使描述很清楚,并且你能够弄清楚 compareTo 方法将输出什么,使用调用该方法的给定 BigDecimal 值和给定的 BigDecimal 输入作为比较值。对于除 BigDecimal 之外的其他类广泛使用 compareTo 方法的人来说,即使他们是特定类的新手,这可能也是显而易见的,但如果您最近没有对 ANY compareTo 方法的结果使用布尔值,那么查看它会更快。

在使用 int 时,你很可能会像这样编写代码:

int x = 5;
x = x + 2;
System.out.println(x) // should be 7

在这里,“2”值从未被声明为 int。加法的结果与我们声明 y=2 并说 x = x+y 而不是 x = x+2 相同,但是在上面的代码行中,没有命名变量或整数对象,如果我们使用这些变量而不是原始整数,则为“2”创建。另一方面,对于 BigDecimal,由于 BigDecimal.add 方法需要 BigDecimal 作为输入,因此必须创建一个等于 2 的 BigDecimal 才能添加 2。我在官方文档中没有看到任何内容说“使用它作为双精度的更准确的替代品,或者如果你想要比 BigInteger 更通用的东西,也可以用它来替代多头,但除了使用它作为声明变量的替代品之外,还可以创建等于小整数的 BigDecimal 对象,这些对象本身不会要求使用 BigDecimal 类,以便您可以在操作中使用它们。如果你想使用 BigDecimals,你的变量和你要添加到它们的小值都需要是 BigDecimals。

最后,让我解释一些有可能使 BigDecimal 类比它需要的更令人生畏的事情。任何曾经使用过原始数组并试图在创建数组时提前预测它需要多大的人,或者熟悉低级语言如何涉及程序员需要确切知道某些东西占用多少字节的某些情况的人,在处理似乎需要预先指定精度级别的事情时,可能会感到需要谨慎。文档是这样说的:“如果未指定舍入模式并且无法表示确切的结果,则会引发异常;否则,可以通过向操作提供适当的 MathContext 对象来执行所选精度和舍入模式的计算。第一次阅读该句子的新手可能会认为,在第一次编写代码时,他们将不得不广泛考虑舍入问题,否则一旦值无法准确表示,就会面临异常,或者他们必须在使用 BigDecimal 之前阅读有关 MathContext 对象的文档, 这反过来可能会导致阅读IEEE标准,这些标准有助于理解浮点数,但与人们实际想要编码的内容相去甚远。看到 BigDecimal 的一些构造函数将数组作为输入,而其他构造函数将 MathContext 作为输入,同时注意到相关 BigInteger 类的构造函数之一将字节数组作为输入,可能会加强这样一种感觉,即使用此对象类需要非常精细地了解将用于该类所用于特定计算的确切位数,并且理解 MathContext 对于即使是最基本的类使用也或多或少是必不可少的。虽然我确信理解 MathContext 会有所帮助,但 baby 的第一个 BigDecimal 项目实际上可以很好地运行,而无需在第一次使用 BigDecimal 的同时学习这个附加功能。读取 scale 参数还可能导致编码人员在第一次查找有关类的信息时认为,必须提前预测答案的数量级才能使用该类。

阿曼多·卡瓦洛(Armando Caballo)值得称赞的回答表明,假设新手的这些担忧被夸大了,因为虽然舍入模式确实需要相当频繁地指定,并且在使用除法时经常将一致的刻度称为参数,但刻度参数实际上是对小数位数方面所需精度的相当任意的规范,而不是需要精确预测类将处理哪些数字(除非使用 BigDecimal 的最终目的需要精细控制的精度级别,在这种情况下,它很容易指定)。处理用于计算弧切线的“无限”系列加法和减法项,而无需声明 MathContext 对象。