When I first studied Basic Arithmetic rational numbers, irrational numbers etc it aroused curiosity in me for days (e.g e + pi will be irrational or not), years later when I encountered Floating Point Arithmetic in computer science (CS), I was again curious on the precision/accuracy of base 2 ? Below is an YouTube video tutorial to brush up the calculation:
As per Wikipedia Floating Point Arithmetic is:
Whether or not a rational number has a terminating expansion depends on the base. For example, in base-10 the number 1/2 has a terminating expansion (0.5) while the number 1/3 does not (0.333…). In base-2 only rationals with denominators that are powers of 2 (such as 1/2 or 3/16) are terminating. Any rational with a denominator that has a prime factor other than 2 will have an infinite binary expansion. This means that numbers which appear to be short and exact when written in decimal format may need to be approximated when converted to binary floating-point. For example, the decimal number 0.1 is not representable in binary floating-point of any finite precision; the exact binary representation would have a “1100” sequence continuing endlessly.
Since a prime factor other than 2 will have an infinite binary expansion, the floating point arithmetic in Java which is used by float and double will always result in imprecise results. This can be explained with the help of following java program:
public class MonetaryDemo
{
public static void main(String[] args)
{
double total = 0.2;
for (int i = 0; i < 100; i++)
{
total += 0.2;
}
System.out.println("------------------------------------------");
System.out.println("double total = " + total);
float floatTotal = 0.2f;
for (int i = 0; i < 100; i++)
{
floatTotal += 0.2f;
}
System.out.println("------------------------------------------");
System.out.println("float Total = " + floatTotal);
System.out.println("------------------------------------------");
System.out
.println("Sum of 10 (0.1f) = " + (0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f + 0.1f));
System.out.println("------------------------------------------");
System.out.println("Precision of float 2.9876543218f : " + 2.9876543218f);
System.out.println("Precision of double 2.9876543218d : " + 2.9876543218d);
System.out.println("------------------------------------------");
System.out.println("0.0175 * 100000 = " + 0.0175 * 100000);
System.out.println("0.0175f * 100000 = " + 0.0175f * 100000);
System.out.println("------------------------------------------");
}
}
And output is :
------------------------------------------
double total = 20.19999999999996
------------------------------------------
float Total = 20.200005
------------------------------------------
Sum of 10 (0.1f) = 1.0000001
------------------------------------------
Precision of float 2.9876543218f : 2.9876542
Precision of double 2.9876543218d : 2.9876543218
------------------------------------------
0.0175 * 100000 = 1750.0000000000002
0.0175f * 100000 = 1750.0
------------------------------------------
If we analyse the above , output should have been 20.20
, but the floating point calculation in double
made it 20.19999999999996
and floating point calculation in float made it 20.000004
. This is evident from other calculations in the above example as well.
Thus for monetary calculations where high precision is required, float
and double doesn’t seem to be correct choice.
A dirty workaround popular among most of the developers is using the smallest unit of currency e.g paise in Indian currency rather than using Rupees, cents in US currency rather than Dollar but this just shifts precision to two or three places at most, doesn't solve the problem though.
What does Precision means? It is the total number of significant digits of a real number.
What does Scale mean ? It specifies the number of digits after the decimal place. For
e.g, 2.8765
has the precision of 5 and the scale of 4.
How does BigDecimal solves the problem ?
It is the most suitable choice as it is base 10.
Precision of Float is 6–7 digits , precision of double is 15–16 digits and BigDecimal scale as per Java 8 docs (source : here):
Immutable, arbitrary-precision signed decimal numbers. A BigDecimal consists of an arbitrary precision integer unscaled value and a 32-bit integer scale. If zero or positive, the scale is the number of digits to the right of the decimal point. If negative, the unscaled value of the number is multiplied by ten to the power of the negation of the scale. The value of the number represented by the BigDecimal is therefore (unscaledValue × 10-scale).
What is the use case of Float ?
Above facts point to the fact that float will have more precision loss
than double, thus arising the question why and when to use float ? Use
case of float is to save memory and better performance of arithmetic,
especially on 32bit architectures. It can be explained as it will show
improved performance over doubles for applications which process large
arrays of floating point numbers such that memory bandwidth is the
limiting factor. By switching to float[]
from double[]
, thus halving the data size, we effectively double the
throughput, because twice as many values can be fetched in a given time.
So the applications where performance is higher priority than precision
, they should prefer float.
Size of various data types:
-
double
: 8 bytes -
Double
: 16 bytes (8 bytes overhead for the class, 8 bytes for the containeddouble
) -
BigDecimal
: 32 bytes -
long
: 8 bytes -
Long
: 16 bytes (8 bytes overhead for the class, 8 bytes for the containedlong
) -
BigInteger
: 56 bytes
Above size reflects one thing that storage requirements become 4 times than double. Now days systems have cheap ram and enough this is no longer a problem.
Example Usage of BigDecimal is as follows:
public class MonetaryDemo
{
public static void main(String args[])
{
double amount1 = 1.15;
double amount2 = 1.10;
System.out.println("Diff between 1.15 and 1.10 using double is: " + (amount1 - amount2));BigDecimal amount3 = new BigDecimal("1.15");
BigDecimal amount4 = new BigDecimal("1.10");
System.out.println("Diff between 1.15 and 1.10 using BigDecimal is: " + (amount3.subtract(amount4)));final long iterations = 10000000;
long t = System.currentTimeMillis();
double d = 789.0123456;
for (int i = 0; i < iterations; i++)
{
final double b = d * ((double) System.currentTimeMillis() + (double) System.currentTimeMillis());
}
System.out.println("Execution time for 10M iterations double: " + (System.currentTimeMillis() - t));t = System.currentTimeMillis();
BigDecimal bd = new BigDecimal("789.0123456");
for (int i = 0; i < iterations; i++)
{
final BigDecimal b = bd.multiply(
BigDecimal.valueOf(System.currentTimeMillis()).add(BigDecimal.valueOf(System.currentTimeMillis())));
}
System.out.println("Execution time for 10M iterations BigDecimal: " + (System.currentTimeMillis() - t));
}
}
output of the above is :
Diff between 1.15 and 1.10 using double is: 0.04999999999999982
Diff between 1.15 and 1.10 using BigDecimal is: 0.05
Execution time for 10M iterations double: 444
Execution time for 10M iterations BigDecimal: 951
Lets analyze the output : execution time of 10 Million iterations of BigDecimal takes more than double time but provides precise results.
Verdict:
If precision is of utmost importance BigDecimal is the way to go even though it has some performance drawbacks.
Points to be Noted:
-
Do not convert
double
toBigDecimal
, instead convertString
toBigDecimal
when possible because usingBigDecimal(double)
is unpredictable due to the inability of the double to represent 0.1 as exact 0.1. - Rounding mode should be provided while setting the scale, else default rounding mode of HALF_EVEN (also known as bankers’ rounding) will be used.
-
Always use
MathContext
forBigDecimal
multiplication and division in order to avoidArithmeticException
for infinitely long decimal results. Don't useMathContext.UNLIMITED
for that reason - it is equivalent to no context at all.
Further Reading :
- Need for BigDecimal :
2) Decimal to IEEE 754 Floating Point Representation for reference https://www.youtube.com/watch?v=8afbTaA-gOQ