Java关于浮点数的舍入法与String.format()在不同jdk版本的区别

1. 前提知识

  1. 浮点数在计算机中是以二进制的形式存储的,大部分浮点数都无法精确表达
  2. 银行家舍入法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一

2. 碰到的问题

我在对浮点数做保留两位小数并四舍五入的操作时,测试的时候发现在JDK 8和Android 5.1上得到的值是不一样的。
代码:

public static void main(String[] args) {
    System.out.println(String.format("5.585f -> %.2f", 5.585f));
    System.out.println(String.format("5.535f -> %.2f", 5.535f));
    System.out.println(String.format("5.545f -> %.2f", 5.545f));
    System.out.println(String.format("5.555f -> %.2f", 5.555f));
    System.out.println("------------------------------");

    System.out.println(String.format("5.585 -> %.2f", Double.valueOf(String.valueOf(5.585f))));
    System.out.println(String.format("5.535 -> %.2f", Double.valueOf(String.valueOf(5.535f))));
    System.out.println(String.format("5.545 -> %.2f", Double.valueOf(String.valueOf(5.545f))));
    System.out.println(String.format("5.555 -> %.2f", Double.valueOf(String.valueOf(5.555f))));
    System.out.println("------------------------------");

    System.out.println(String.format("558.5f -> %.0f", 558.5f));
    System.out.println(String.format("553.5f -> %.0f", 553.5f));
    System.out.println(String.format("554.5f -> %.0f", 554.5f));
    System.out.println(String.format("555.5f -> %.0f", 555.5f));
    System.out.println("------------------------------");

    System.out.println("Math.round(558.5f) -> " + Math.round(558.5f));
    System.out.println("Math.round(553.5f) -> " + Math.round(553.5f));
    System.out.println("Math.round(554.5f) -> " + Math.round(554.5f));
    System.out.println("Math.round(555.5f) -> " + Math.round(555.5f));
    System.out.println("------------------------------");

    System.out.println("BigDecimal(5.585) -> " + new BigDecimal(5.585).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(5.535) -> " + new BigDecimal(5.535).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(5.545) -> " + new BigDecimal(5.545).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(5.555) -> " + new BigDecimal(5.555).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("------------------------------");

    System.out.println("BigDecimal(\"5.585\") -> " + new BigDecimal(String.valueOf(5.585f)).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(\"5.535\") -> " + new BigDecimal(String.valueOf(5.535f)).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(\"5.545\") -> " + new BigDecimal(String.valueOf(5.545f)).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("BigDecimal(\"5.555\") -> " + new BigDecimal(String.valueOf(5.555f)).setScale(2, BigDecimal.ROUND_HALF_UP));
    System.out.println("------------------------------");
}

Android 5.1上的运行结果:
Java关于浮点数的舍入法与String.format()在不同jdk版本的区别_第1张图片
jdk 8上的运行结果:
Java关于浮点数的舍入法与String.format()在不同jdk版本的区别_第2张图片

3. 在验证的过程中我得到了以下几个结论:

  1. String.format(“%f”, xxx) 这里传入float或者double,最后都换强制转换成double去计算,所以传入float可能会造成精度损失(下文会验证)
  2. 不同jdk版本的String.format对浮点数的舍入方式不同
  3. 应该用BigDecimal(String str)来做高精度的运算

4. 证明1:精度损失

ps:我测试的java 8和android 5.1中都这样处理
在Java 8中,String.format()会调用到Formatter.format()这个方法,在Formatter.format()中又调用到了Formatter.FormatSpecifier.print(),看其中的printFloat()方法

private void printFloat(Object arg, Locale l) throws IOException {
    if (arg == null)
        print("null");
    else if (arg instanceof Float)
        print(((Float)arg).floatValue(), l);
    else if (arg instanceof Double)
        print(((Double)arg).doubleValue(), l);
    else if (arg instanceof BigDecimal)
        print(((BigDecimal)arg), l);
    else
        failConversion(c, arg);
}

private void print(float value, Locale l) throws IOException {
    print((double) value, l);
}

private void print(double value, Locale l) throws IOException {
    StringBuilder sb = new StringBuilder();
    boolean neg = Double.compare(value, 0.0) == -1;

    if (!Double.isNaN(value)) {
        double v = Math.abs(value);

        // leading sign indicator
        leadingSign(sb, neg);

        // the value
        if (!Double.isInfinite(v))
            print(sb, v, l, f, c, precision, neg);
        else
            sb.append(f.contains(Flags.UPPERCASE)
                      ? "INFINITY" : "Infinity");

        // trailing sign indicator
        trailingSign(sb, neg);
    } else {
        sb.append(f.contains(Flags.UPPERCASE) ? "NAN" : "NaN");
    }

    // justify based on width
    a.append(justify(sb.toString()));
}

可以看到参数为float,强制转换成double然后调用print(double)的方法,这里就会导致精度的损失
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述

5. 证明2:不同jdk版本的String.format对浮点数的舍入方式不同

1.在java 8中
从上面的print()中继续跟进,进入到这个方法

private void print(StringBuilder sb, double value, Locale l,
                           Flags f, char c, int precision, boolean neg) {
    if(...) {
        // ...
    } else if(...) {
        // ...
    } else if (c == Conversion.DECIMAL_FLOAT) {
        // Create a new FormattedFloatingDecimal with the desired
        // precision.
        int prec = (precision == -1 ? 6 : precision);

        FormattedFloatingDecimal fd
                = FormattedFloatingDecimal.valueOf(value, prec,
                  FormattedFloatingDecimal.Form.DECIMAL_FLOAT);

        char[] mant = addZeros(fd.getMantissa(), prec);

        // If the precision is zero and the '#' flag is set, add the
        // requested decimal point.
        if (f.contains(Flags.ALTERNATE) && (prec == 0))
            mant = addDot(mant);

        int newW = width;
        if (width != -1)
            newW = adjustWidth(width, f, neg);
        localizedMagnitude(sb, mant, f, newW, l);
    } else if(...) {
        // ...
    }
}

在FormattedFloatingDecimal.valueOf()中进入FloatingDecimal.getBinaryToASCIIConverter()这个方法,结合FormattedFloatingDecimal类的构造函数,可以大致看到结果是在这里产生的。由Java代码计算而没有调用c的format方法。(这块的代码看的有点懵,如果有细看过的求解释!)

2.在android 5.1中
在Formatter类中的transfrom()方法中,这个demo会进入transformFromFloat()方法,然后执行transfromF()

private void transformF(StringBuilder result) {
    // All zeros in this method are *pattern* characters, so no localization.
    String pattern = "0.000000";
    final int precision = formatToken.getPrecision();
    if (formatToken.flagComma || precision != FormatToken.DEFAULT_PRECISION) {
        StringBuilder patternBuilder = new StringBuilder();
        if (formatToken.flagComma) {
            patternBuilder.append(',');
            int groupingSize = 3;
            char[] sharps = new char[groupingSize - 1];
            Arrays.fill(sharps, '#');
            patternBuilder.append(sharps);
        }
        patternBuilder.append('0');
        if (precision > 0) {
            patternBuilder.append('.');
            for (int i = 0; i < precision; ++i) {
                patternBuilder.append('0');
            }
        }
        pattern = patternBuilder.toString();
    }

    NativeDecimalFormat nf = getDecimalFormat(pattern);
    if (arg instanceof BigDecimal) {
        result.append(nf.formatBigDecimal((BigDecimal) arg, null));
    } else {
        result.append(nf.formatDouble(((Number) arg).doubleValue(), null));
    }
    // The # flag requires that we always output a decimal separator.
    if (formatToken.flagSharp && precision == 0) {
        result.append(localeData.decimalSeparator);
    }
}

在NativeDecimalFormat中最后会调用本地方法

6. 其他的容易碰到的问题

1.上述的情况我也在android 7.0中做了测试,结果与jdk 8中的一致,而与android 5.1中的不一致,所以如果采用不严格的舍入法会导致在不同平台上出现数据上的差别。
Java关于浮点数的舍入法与String.format()在不同jdk版本的区别_第3张图片
2.BigDecimal(double)也会出现精度损失的问题(我没有细看为什么),应该使用BigDecimal(String)

总结

  1. String.format()在不同jdk中的实现不一样,会出现浮点数四舍五入不一致的情况,应该使用BigDecimal(String)来做
  2. String.format()中传入float或double,最后都会转成double来处理,所以使用的时候要注意

2017.11.1更新

DecimalFormat#format
代码:

public static void main(String[] args) {
    System.out.println(new DecimalFormat("#.00").format(5.205));
    System.out.println(new DecimalFormat("#.00").format(5.215));
    System.out.println(new DecimalFormat("#.00").format(5.225));
    System.out.println(new DecimalFormat("#.00").format(5.235));
    System.out.println(new DecimalFormat("#.00").format(5.245));
    System.out.println(new DecimalFormat("#.00").format(5.255));
    System.out.println(new DecimalFormat("#.00").format(5.265));
    System.out.println(new DecimalFormat("#.00").format(5.275));
    System.out.println(new DecimalFormat("#.00").format(5.285));
    System.out.println(new DecimalFormat("#.00").format(5.295));
}

在Android 8.0 & 4.4.4 & 4.3 中,都是银行家算法,最后调用NativeDecimalFormat#formatDouble(本地方法)
运行结果:
D/MainActivity: 5.20
D/MainActivity: 5.22
D/MainActivity: 5.22
D/MainActivity: 5.24
D/MainActivity: 5.24
D/MainActivity: 5.26
D/MainActivity: 5.26
D/MainActivity: 5.28
D/MainActivity: 5.28
D/MainActivity: 5.30

在Java1.8中,也是银行家算法,但是结果不一致(精度丢失?)…代码太深了调试不进去了。。
运行结果:
5.21
5.21
5.22
5.24
5.25
5.25
5.26
5.28
5.29
5.29

你可能感兴趣的:(Android,Java)