如何在Java中将数字舍入到n位小数

1462
我想要的是一种将双精度浮点数转换为字符串的方法,该方法使用"四舍五入" - 即如果要舍入的小数为5,则始终向上舍入到下一个数字。这是大多数情况下人们预期的标准舍入方法。同时,只显示有效数字 - 即不应有任何尾随零。
我知道实现此功能的一种方法是使用String.format方法:
String.format("%.5g%n", 0.912385);

返回值:

0.91239

这很不错,但它总是显示带有5位小数的数字,即使它们并不重要:

String.format("%.5g%n", 0.912300);

返回:

0.91230

另一种方法是使用DecimalFormatter

DecimalFormat df = new DecimalFormat("#.#####");
df.format(0.912385);

返回:

0.91238

不过,正如您所看到的,这里使用的是"半偶数"舍入法。也就是说,如果前一个数字是偶数,则会将其向下舍入。我希望的是:

0.912385 -> 0.91239
0.912300 -> 0.9123

在Java中实现这一目标的最佳方法是什么?

39个回答

882

使用setRoundingMode方法,将RoundingMode明确设置为处理您对半偶数舍入问题的解决方案,然后使用所需输出的格式模式。

例如:

DecimalFormat df = new DecimalFormat("#.####");
df.setRoundingMode(RoundingMode.CEILING);
for (Number n : Arrays.asList(12, 123.12345, 0.23, 0.1, 2341234.212431324)) {
    Double d = n.doubleValue();
    System.out.println(df.format(d));
}

输出结果为:

12
123.1235
0.23
0.1
2341234.2125

编辑: 原回答未涉及双精度值的准确性。如果您不太关心它是向上还是向下舍入,那么这没关系。但是如果您想进行精确舍入,则需要考虑值的期望精度。浮点值在内部具有二进制表示法。这意味着像2.7735这样的值在内部实际上并没有这个确切的值。它可能略大或略小。如果内部值稍微小一些,那么它就不会舍入到2.7740。为了解决这种情况,您需要了解正在使用的值的准确性,并在舍入之前添加或减去该值。例如,当您知道您的值的准确度高达6位数字时,为了将中间值舍入上去,可以将该准确度添加到该值中:

Double d = n.doubleValue() + 1e-6;

向下取整,减去精度。


10
到目前为止,这可能是最好的解决方案。当我第一次查看 DecimalFormat 类时,我没有发现这个功能的原因是它只在 Java 1.6 中引入。不幸的是,我被限制使用 1.5 版本,但将来知道这个功能会很有用。 - Alex Spurling
6
请注意,DecimalFormat 取决于您当前的本地配置,因此您可能不会得到点作为分隔符。我个人更喜欢下面 Asterite 的回答。 - Gomino
1
还要注意,您不应该期望DecimalFormat是线程安全的。根据Java文档十进制格式通常不同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一个格式,则必须在外部进行同步。 - CGK
2
如何使它进行正确的四舍五入,以便不会将0.0004四舍五入为0.001。 - user4919188
1
创建这种情况并不太困难,这种方法无法保证正确结果,如果结果的准确性很重要,我不会使用这种方法。 - Dawood ibn Kareem
显示剩余5条评论

524

假设value是一个double,你可以这样做:

(double)Math.round(value * 100000d) / 100000d

这是为了保留5位数字精度。零的数量表示小数点后的位数。


85
更新:我刚刚确认,这种方法比使用DecimalFormat快得多。我使用DecimalFormat循环了200次,而使用这种方法。 DecimalFormat完成200个循环需要14毫秒,而这种方法少于1毫秒。正如我所料,这更快。如果你按时钟周期计费,这就是你应该做的事情。老实说,我对Chris Cudmore所说的话感到惊讶。分配对象总是比转换基本类型和使用静态方法(Math.round()与decimalFormat.format()相比)更昂贵。 - Andi Jay
121
这种技术在超过90%的情况下失败。 - user207421
28
确实,这个表达式失败了:Math.round(0.1 * Math.pow(10,20))/Math.pow(10,20) == 0.09223372036854775 - Robert Tupelo-Schneck
68
使用这种方法(或任何浮点数舍入方法)时要非常小心。即使对于像 265.335 这样简单的数字也可能会出现问题。使用精度为 2 位小数的乘法计算 265.335 * 100 的中间结果为 26533.499999999996。这意味着它被舍入到了 265.33。从浮点数转换为真实十进制数时存在固有的问题。请参见 EJP 在 https://dev59.com/-nVC5IYBdhLWcg3w4Vb6#12684082 中的回答。 - Sebastiaan van den Broek
13
@SebastiaanvandenBroek:哇,我从来不知道得到错误答案是那么容易的事情。但是,如果使用非精确数字进行工作,必须认识到任何值都不是精确的265.335实际上意味着265.335 += 公差,其中公差取决于以前的操作和输入值的范围。我们不知道真正的、精确的值。在边界值处,任一答案都可以被认为是正确的。如果我们需要精确,我们就不应该使用double数据类型。这里的fail不在于转换回double,而是源代码编写者认为传入的265.335是确切的数值。 - ToolmakerSteve
显示剩余17条评论

211
new BigDecimal(String.valueOf(double)).setScale(yourScale, BigDecimal.ROUND_HALF_UP);

将会得到一个BigDecimal对象。要从中获取字符串,只需调用该BigDecimal对象的toString方法,或者对于Java 5+,调用toPlainString方法获取普通格式的字符串。

示例程序:

package trials;
import java.math.BigDecimal;

public class Trials {

    public static void main(String[] args) {
        int yourScale = 10;
        System.out.println(BigDecimal.valueOf(0.42344534534553453453-0.42324534524553453453).setScale(yourScale, BigDecimal.ROUND_HALF_UP));
    }

43
这是我首选的解决方案。更简洁地说: BigDecimal.valueOf(doubleVar).setScale(yourScaleHere, BigDecimal.ROUND_HALF_UP);实际上,BigDecimal.valueOf(double val)在幕后调用了Double.toString() ;) - Etienne Neveu
4
好的。不要走捷径并使用new BigDecimal(doubleVar),因为您可能会遇到浮点数舍入问题。 - Edd
8
有趣的是,在SebastiaanvandenBroek评论asterite答案中提到的情况下会出现舍入问题。double val = 265.335;BigDecimal.valueOf(val).setScale(decimals, BigDecimal.ROUND_HALF_UP).toPlainString(); => 265.34,但(new BigDecimal(val)).setScale(decimals, BigDecimal.ROUND_HALF_UP).toPlainString(); => 265.33 - ToolmakerSteve
7
这是因为使用带有double参数的new BigDecimal方法直接将double值传递给BigDecimal并尝试创建BigDecimal对象,而使用BigDecimal.valueOf或toString方法则先将其解析为字符串(更准确的表示),然后再进行转换。 - MetroidFan2002
1
BigDecimal.ROUND_HALF_UP自9版本起已被弃用。 您可以使用RoundingMode.HALF_UP代替。 - Niels Ferguson
显示剩余2条评论

134
您可以使用以下方法:

DecimalFormat df = new DecimalFormat("#.00000");
df.format(0.912385);

确保您有末尾的 0。


26
我相信问题的一个目标是“不应该有任何末尾的零”。 - Lunchbox
9
对于这个问题,提问者不想要零,但这正是我需要的。如果你有一组保留三位小数的数字,即使是0,也希望它们都具有相同的数字。 - Tom Kincaid
您忘记指定“RoundingMode”。 - IgorGanapolsky
1
默认情况下,Decimal mode 使用 RoundingMode.HALF_EVEN - EndermanAPM

96

正如其他人所指出的,正确的答案是使用DecimalFormatBigDecimal。浮点数并没有十进制位,因此您根本无法首先将其舍入/截断为特定数量的十进制位。您必须以十进制基数工作,并且这就是这两个类所做的。

我发布以下代码作为对本主题中所有答案以及StackOverflow(以及其他地方)中建议先进行乘法,然后进行截断,最后进行除法的反例。支持这种技术的倡导者有责任解释为什么以下代码在超过92%的情况下会产生错误输出。

public class RoundingCounterExample
{

    static float roundOff(float x, int position)
    {
        float a = x;
        double temp = Math.pow(10.0, position);
        a *= temp;
        a = Math.round(a);
        return (a / (float)temp);
    }

    public static void main(String[] args)
    {
        float a = roundOff(0.0009434f,3);
        System.out.println("a="+a+" (a % .001)="+(a % 0.001));
        int count = 0, errors = 0;
        for (double x = 0.0; x < 1; x += 0.0001)
        {
            count++;
            double d = x;
            int scale = 2;
            double factor = Math.pow(10, scale);
            d = Math.round(d * factor) / factor;
            if ((d % 0.01) != 0.0)
            {
                System.out.println(d + " " + (d % 0.01));
                errors++;
            }
        }
        System.out.println(count + " trials " + errors + " errors");
    }
}

这个程序的输出:

10001 trials 9251 errors

编辑:为了回应下面的一些评论,我重新使用 BigDecimalnew MathContext(16) 来重做测试循环中的模数部分,代码如下:

public static void main(String[] args)
{
    int count = 0, errors = 0;
    int scale = 2;
    double factor = Math.pow(10, scale);
    MathContext mc = new MathContext(16, RoundingMode.DOWN);
    for (double x = 0.0; x < 1; x += 0.0001)
    {
        count++;
        double d = x;
        d = Math.round(d * factor) / factor;
        BigDecimal bd = new BigDecimal(d, mc);
        bd = bd.remainder(new BigDecimal("0.01"), mc);
        if (bd.multiply(BigDecimal.valueOf(100)).remainder(BigDecimal.ONE, mc).compareTo(BigDecimal.ZERO) != 0)
        {
            System.out.println(d + " " + bd);
            errors++;
        }
    }
    System.out.println(count + " trials " + errors + " errors");
}

结果:

10001 trials 4401 errors

10
关键在于,在你的所有9251个错误中,打印出的结果仍然是正确的。 - Didier L
8
@DidierL 这并不让我感到惊讶。我非常幸运地在我的第一门计算机课程中学习了“数值方法”,并在开始时就被介绍了浮点数可以做什么和不能做什么。大多数程序员对此都很模糊。 - user207421
17
你所做的只是驳斥浮点数不能准确表达许多小数值,这一点希望我们都能理解。而不是舍入会导致问题。正如你承认的那样,数字仍然按预期打印出来。 - Peter Lawrey
10
你的测试有问题,如果去掉 round() 函数,这个测试会在 94% 的时间内失败。http://ideone.com/1y62CY 输出结果为 100 次测试中有 94 次错误。你应该从一个可以通过的测试开始,并展示引入 rounding 后测试是如何失效的。 - Peter Lawrey
7
这里是驳斥,被驳斥了。在这个double范围内使用Math.round没有错误。http://ideone.com/BVCHh3 - Peter Lawrey
显示剩余15条评论

93

假设你有

double d = 9232.129394d;
你可以使用BigDecimal
BigDecimal bd = new BigDecimal(d).setScale(2, RoundingMode.HALF_EVEN);
d = bd.doubleValue();

或者不使用BigDecimal

d = Math.round(d*100)/100.0d;

使用这两种方法,d == 9232.13


2
我认为这是Java 1.5用户(以及更低版本)的最佳解决方案。不过有一点要注意,不要使用HALF_EVEN舍入模式,因为它对奇数和偶数的数字具有不同的行为(例如2.5四舍五入到2而5.5四舍五入到6),除非这是您想要的。 - IcedDante
5
第一种解决方案是正确的:第二种不起作用。请参见此处获取证明。 - user207421
1
@EJP:即使是使用RoundingMode.HALF_UP的第一种解决方案也是错误的。尝试使用1.505进行测试。正确的方法是使用BigDecimal.valueOf(d) - Matthias Braun
1
用 BigDecimal(耗时 225 毫秒)和 Math.round(2 毫秒)的方式运行了 100k 的循环,以下是所用时间... 转换所需时间:使用 to 方法转换为 9232.13,共计 225 毫秒 转换所需时间:转换为 9232.13,共计 2 毫秒 http://www.techiesinfo.com - user1114134
这个解决方案比其他方法慢了112倍。我猜如果你不在意编写快速代码,为什么一开始要使用double呢?如果你想要正确的十进制数字,那么永远不要一开始就使用double。 - hamish
显示剩余4条评论

64

你可以使用DecimalFormat类。

double d = 3.76628729;

DecimalFormat newFormat = new DecimalFormat("#.##");
double twoDecimal =  Double.valueOf(newFormat.format(d));

为什么选择Double.valueOf()而不是Double.parseDouble()valueOf()方法返回一个Double对象,而parseDouble()将返回一个double原始类型。根据当前代码的编写方式,您还需要应用自动拆箱来将其转换为您的twoDouble变量所期望的原始类型,这是一种额外的字节码操作。我会更改答案,使用parseDouble()代替。 - ecbrodie
Double.parseDouble() 需要一个 String 类型的输入。 - Ryde

48

Real's Java How-to 文章提供了这个解决方案,该方案也适用于Java 1.6之前的版本。

BigDecimal bd = new BigDecimal(Double.toString(d));
bd = bd.setScale(decimalPlace, BigDecimal.ROUND_HALF_UP);
return bd.doubleValue();

更新:BigDecimal.ROUND_HALF_UP已被弃用 - 请使用RoundingMode

BigDecimal bd = new BigDecimal(Double.toString(number));
bd = bd.setScale(decimalPlaces, RoundingMode.HALF_UP);
return bd.doubleValue();

32
double myNum = .912385;
int precision = 10000; //keep 4 digits
myNum= Math.floor(myNum * precision +.5)/precision;

2
是的,这正是math.round对于正数所做的事情,但你是否尝试过将其应用于负数?其他解决方案中的人们正在使用math.round来处理负数情况。 - hamish
注意:Math.floor(x + 0.5)Math.round(x) - Peter Lawrey

31

@Milhous:使用十进制格式进行四舍五入非常好:

您还可以使用

DecimalFormat df = new DecimalFormat("#.00000");
df.format(0.912385);

我想补充一下,这种方法非常适合提供实际数字的四舍五入机制,不仅在视觉上,而且在处理时也很好用。

假设你需要将四舍五入机制实现到GUI程序中。只需更改插入符格式(即方括号内)即可更改结果输出的精度/准确性。

为了确保您有尾随的0,请按如下方法:

DecimalFormat df = new DecimalFormat("#0.######");
df.format(0.912385);

将作为输出返回:0.912385

DecimalFormat df = new DecimalFormat("#0.#####");
df.format(0.912385);

输出将为:0.91239

DecimalFormat df = new DecimalFormat("#0.####");
df.format(0.912385);

输出结果将为:0.9124

[编辑:如果插入的格式为"^0.############",并输入一个十进制数,比如说3.1415926,DecimalFormat会不会产生垃圾(例如尾随零),并将返回:3.1415926。如果你喜欢这种方式,那就可以用这种方式。虽然有点啰嗦,但在处理过程中具有低内存占用和非常容易实现的优点。]

因此,DecimalFormat的优美之处在于它同时处理了字符串外观和设置的四舍五入精度级别。因此,您只需一份代码实现,就可以获得两个优点。;)


2
如果你真的想在计算中使用小数(而不是仅仅为了输出),请不要使用基于二进制的浮点格式,如double。 而是使用BigDecimal或其他基于十进制的格式。 - Paŭlo Ebermann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接