我在对浮点数做保留两位小数并四舍五入的操作时,测试的时候发现在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上的运行结果:
jdk 8上的运行结果:
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)的方法,这里就会导致精度的损失
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中最后会调用本地方法
1.上述的情况我也在android 7.0中做了测试,结果与jdk 8中的一致,而与android 5.1中的不一致,所以如果采用不严格的舍入法会导致在不同平台上出现数据上的差别。
2.BigDecimal(double)也会出现精度损失的问题(我没有细看为什么),应该使用BigDecimal(String)
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