Wednesday, January 13, 2010

Caution: Double to BigDecimal in Java

The combination of Java's large worldwide developer base and easily accessible online API documentation has led to a generally thorough and accurate documentation of the Java SE API. There are still corners that might not be as thorough or accurate as one would like, but the API documentation is generally pretty good both in terms of thoroughness and accuracy.

Although the Javadoc-based API documentation has become pretty useful, we developers are often in such a hurry and often feel so confident in our own abilities that it is almost inevitable that we will sometimes continue to try to do things without first reading the manual. Because of this tendency, we can occasionally get burned by misusing a particular API despite the documentation warning us not to (mis)use it that way. I discussed this in my blog post on Boolean.getBoolean(String) and highlight a similar issue in this post related to use of BigDecimal's constructor that accepts a double.

At first sight, it might appear that the BigDecimal constructor that accepts a Java double would hold it with its originally specified precision in all cases. However, the Javadoc message for this constructor explicitly warns, "The results of this constructor can be somewhat unpredictable." It goes on to explain why (the double cannot hold the exact precision and this is made evident when passed to the BigDecimal constructor) and to suggest that the alternative constructor accepting a String as a parameter be used instead. The documentation also proposes using BigDecimal.valueOf(double) as the preferred way to convert a double or float to a BigDecimal.

The following code listing is used to demonstrate these principles and a few related ideas.

import java.math.BigDecimal;
import static java.lang.System.out;

* Simple example of problems associated with using BigDecimal constructor
* accepting a double.
public class DoubleToBigDecimal
private final static String NEW_LINE = System.getProperty("line.separator");

public static void main(final String[] arguments)
// Demonstrate BigDecimal from double
final double primitiveDouble = 0.1;
final BigDecimal bdPrimDoubleCtor = new BigDecimal(primitiveDouble);
final BigDecimal bdPrimDoubleValOf = BigDecimal.valueOf(primitiveDouble);
final Double referenceDouble = Double.valueOf(0.1);
final BigDecimal bdRefDoubleCtor = new BigDecimal(referenceDouble);
final BigDecimal bdRefDoubleValOf = BigDecimal.valueOf(referenceDouble);

out.println("Primitive Double: " + primitiveDouble);
out.println("Reference Double: " + referenceDouble);
out.println("Primitive BigDecimal/Double via Double Ctor: " + bdPrimDoubleCtor);
out.println("Reference BigDecimal/Double via Double Ctor: " + bdRefDoubleCtor);
out.println("Primitive BigDecimal/Double via ValueOf: " + bdPrimDoubleValOf);
out.println("Reference BigDecimal/Double via ValueOf: " + bdRefDoubleValOf);


// Demonstrate BigDecimal from float
final float primitiveFloat = 0.1f;
final BigDecimal bdPrimFloatCtor = new BigDecimal(primitiveFloat);
final BigDecimal bdPrimFloatValOf = BigDecimal.valueOf(primitiveFloat);
final Float referenceFloat = Float.valueOf(0.1f);
final BigDecimal bdRefFloatCtor = new BigDecimal(referenceFloat);
final BigDecimal bdRefFloatValOf = BigDecimal.valueOf(referenceFloat);

out.println("Primitive Float: " + primitiveFloat);
out.println("Reference Float: " + referenceFloat);
out.println("Primitive BigDecimal/Float via Double Ctor: " + bdPrimFloatCtor);
out.println("Reference BigDecimal/Float via Double Ctor: " + bdRefFloatCtor);
out.println("Primitive BigDecimal/Float via ValueOf: " + bdPrimFloatValOf);
out.println("Reference BigDecimal/Float via ValueOf: " + bdRefFloatValOf);


// More evidence of issues casting from float to double.
final double primitiveDoubleFromFloat = 0.1f;
final Double referenceDoubleFromFloat = new Double(0.1f);
final double primitiveDoubleFromFloatDoubleValue = new Float(0.1f).doubleValue();

out.println("Primitive Double from Float: " + primitiveDoubleFromFloat);
out.println("Reference Double from Float: " + referenceDoubleFromFloat);
out.println("Primitive Double from FloatDoubleValue: " + primitiveDoubleFromFloatDoubleValue);

// Using String to maintain precision from float to BigDecimal
final String floatString = String.valueOf(new Float(0.1f));
final BigDecimal bdFromFloatViaString = new BigDecimal(floatString);
out.println("BigDecimal from Float via String.valueOf(): " + bdFromFloatViaString);

The output from running the above code is shown in the next screen snapshot.

As the output above indicates, the problem of casting float to double prevents one from retaining the desired precision when passing a float directly to the BigDecimal.valueOf(double) method. A String can be used as an intermediary to accomplish this shown in the example and as demonstrated in similar fashion in Converting Float to Double in a Not So Common Way.

Note that Groovy's heavy implicit use of BigDecimal changes the game a little bit when using Groovy and dynamic typing. I may touch on that in a future blog post. For more details on floating-point issues (and I emphasize "details"), see What Every Computer Scientist Should Know About Floating-Point Arithmetic.

No comments: