Effective Java with Groovy - When float and double put you in trouble!

Let's start with a Java program! What do you think the following program would print?

float price = 0.1f;
float amount = 0f;
for (int i = 0; i < 10; i++) {
    amount += price;
}
System.out.println("Total price: " + amount);

Well, the developer indeed intended the amount to have a value of 1.0, however the program prints "Total price: 1.0000001". If you are a user of a shopping website, would you be happy looking at the result? Definitely not.

Usually, when developers see the above result, they want to try using 'double' instead of 'float' for price and amount. Let's go ahead and try the same.

double price = 0.1;
double amount = 0;
for (int i = 0; i < 10; i++) {
    amount += price;
}
System.out.println("Total price: " + amount);

The above program prints "Total price: 0.9999999999999999". Now, if you own a business, would you be happy if your application produces such a result? Obviously not.

Several developers have written code like the one we saw above and ended up discovering the issues only in production. If you have worked on an application that deals with currency, I am sure you know the problem here. The types float and double are not great at representing arbitrary-precision decimal numbers. If you want to get a deeper understanding of how floating-point numbers are represented, take a look at https://en.wikipedia.org/wiki/IEEE_754

Let's use BigDecimal to represent price and amount.

BigDecimal price = new BigDecimal(0.1);
BigDecimal amount = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
    amount = amount.add(price);
}
System.out.println("Total price: " + amount);

Hold on! Don't relax. The above program gives us "Total price: 1.0000000000000000555111512312578270211815834045410156250". If you had gone through all of these, I am sure you would have grabbed a cup of coffee, or a had good night's sleep before proceeding. The problem here is - Since the precision was already lost when we stored the value '0.1' is double, BigDecimal couldn't do anything about it. Instead, let's pass "0.1" as a string value to the constructor of BigDecimal.

BigDecimal price = new BigDecimal("0.1");
BigDecimal amount = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
    amount = amount.add(price);
}
System.out.println("Total price: " + amount);

Time to breath a sigh of relief! We finally managed to get "Total price: 1.0"

As an alternative to passing string value to the constructor, you could also make use of the valueOf method as follows.

BigDecimal price = BigDecimal.valueOf(0.1);
BigDecimal amount = BigDecimal.ZERO;
for (int i = 0; i < 10; i++) {
    amount = amount.add(price);
}
System.out.println("Total price: " + amount);

The valueOf method translates a double into a BigDecimal, using the double's canonical string representation provided by the Double.toString(double) method.

Without further delay, let's write the code in Groovy.

def price = 0.1
def amount = 0
10.times {
    amount += price
}
println "Amount: $amount"

The above Groovy code produces "Amount: 1.0" Note how we did not specify the concrete types for price and amount and made use of the type inference for our benefit. Groovy by default, uses BigDecimal type for representing decimal numbers. Let's add a few print statements to confirm the inferred types.

package com.nareshak.demo

def price = 0.1
def amount = 0
10.times {
    amount += price
}
println "Amount: $amount"
println price.class
println amount.class

The above code produces the following results.

Amount: 1.0
class java.math.BigDecimal
class java.math.BigDecimal

Groovy here rightly assumes a sensible default type (which is BigDecimal) saving developers from committing costly mistakes. We typically call this "the principle of least astonishment".

If you are interested in learning more about how Effective Java applies to Groovy code, you may want to take a look at my GR8Conf EU presentation.

References:

Show Comments