How safe BigDecimal really is?
· 4 min read
BigDecimal is the go-to type for holding decimal numbers. It's used almost every time high precision is required, and we can't afford rounding issues. But not everything with it is so great. BigDecimal exposes a trap for developers. Let's take a look at what that is and how to secure our code against it.
Why do we even need BigDecimal?
Doubles are not the greatest when it comes to computation with precision. Take a look at this example -
we want to add three 0.1
, the outcome should be 0.3
, but it's not...
In some cases that might not be an issue. For instance, if we want to know the average temperature for the past last ten
days - we can live with that slight offset. But in some other cases, we can't have any discrepancies - for instance, if we are
dealing with money. Luckily Java comes with BigDecimal
type to help with that issue. Adding three BigDecimal(0.1)
indeed
give us the correct result BigDecimal(0.3)
. Perfect! This is what BigDecimal is for.
Does it mean that case is closed? BigDecimal
is a safe type to perform arithmetic calculations?
Yes and no. Indeed BigDecimal
gives us a precise result, but there is a pitfall, that we need to talk about.
Let's review a couple of different ways of initiating the BigDecimal.
Every single way we initialize the BigDecimal
gives us a result that we expected, except the first one.
Passing double to the constructor seems like a default way of creating BigDecimal
- and probably is the
first approach someone would try if they have to make a BigDecimal
. But the outcome is completely off.
To know why that is, let's read the comment on that constructor.
The very first sentence illustrates our situation perfectly:
The results of this constructor can be somewhat unpredictable.
Oh yes, we can tell. The rest of the doc explains why it works this way, you can read the whole thing here
The default constructor is unpredictable. Unpredictability is something we would like to avoid at all costs.
So let's avoid using that constructor altogether. BigDecimal.valueOf(0.1)
doesn't have the same problem since
JVM will first cast double
to String
before passing it into the constructor. The same approach is used by
Kotlin with 0.1.toBigDecimal()
helper. Those two methods are safe to use.
But having an unpredictable constructor in a mix has further-reaching consequences for our code.
Let's take a look at an example where we might want to use BigDecimal
. The Value Object that will
store Money. The most straightforward way of doing so in Kotlin might look like this:
Please notice that we are accepting BigDecimal
as a parameter. Hence, we are accepting object that has
been already built and we don't have any control over how. And as we know there is a bad and good way of
creating BigDecimal
. To prevent our Money object users from getting themself into trouble we need to construct
our code in a way that it will be impossible to use badly-constructed BigDecimal
.
In the new updates class, we expect the user to provide the amount as double - never as BigDecimal
.
Double
is then transformed to BigDecimal
in a controlled way and stored inside a value object. We store it as
BigDecimal
since we still prefer to make a computation on BigDecimal
, not the double
.
Also, we are removing data
from the class. Leaving data
will add .clone()
method that will allow clients to
overcome the constructor and pass BigDecimal
directly inside the model.
TL;DR
BigDecimal
is safe to perform arithmetic operations. But one of the constructors is working unpredictably.
Therefore we should never accept BigDecimal
as a param into the Value Object. Accept primitive values and
take care of constructing BigDecimal
by yourself.
If you are using Sonar Cube one of the default rules will check for any calls to the default constructor and highlight them as errors.