How safe is BigDecimal, really?
BigDecimal is the go‑to type for holding decimal numbers. It’s used whenever high precision is required and we can’t afford rounding issues. But not everything about it is great. BigDecimal exposes a trap for developers. Let’s look at what that is and how to secure our code against it.
Why do we even need BigDecimal?
Double is not great for precise decimal arithmetic. Take a look at this example:
we want to add three 0.1
s; the outcome should be 0.3
, but it’s not…
fun main() { //sampleStart val sum = 0.1 + 0.1 + 0.1 println(sum) // 0.30000000000000004 //sampleEnd }
In some cases, that might not be an issue. For instance, if we want to know the average temperature for the last ten
days—we can live with that slight offset. But in other cases, we can’t have any discrepancies—for instance, when dealing with money. Luckily, Java comes with the BigDecimal
type to help with that issue. Using BigDecimal
(after converting 0.1 to BigDecimal first) indeed
gives us the correct result: 0.3
. Perfect! This is what BigDecimal is for.
fun main() { //sampleStart val sum = 0.1.toBigDecimal() + 0.1.toBigDecimal() + 0.1.toBigDecimal() println(sum) // 0.3 //sampleEnd }
Does it mean the case is closed? Is BigDecimal
a safe type to perform arithmetic?
Yes and no. Indeed, BigDecimal
gives us a precise result, but there is a pitfall we need to talk about.
Let’s review a few different ways of initializing a BigDecimal.
import java.math.BigDecimal import java.math.MathContext fun main() { //sampleStart println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827021181583404541015625 println(BigDecimal.valueOf(0.1)) // 0.1 println(BigDecimal("0.1")) // 0.1 println(0.1.toBigDecimal()) // 0.1 println(BigDecimal(0.1, MathContext.DECIMAL64)) // 0.1000000000000000 //sampleEnd }
Every way we initialize the BigDecimal
gives us the result we expect—except the first one.
Passing a double
to the constructor seems like the default way of creating a BigDecimal
—and it’s probably the
first approach someone tries. 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 double‑argument constructor is unpredictable. Unpredictability is something we’d 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 the
JVM converts the double
to a String
before passing it into the constructor. The same approach is used by
Kotlin with the 0.1.toBigDecimal()
helper. Those two methods are safe to use.
But having an unpredictable constructor in the mix has further‑reaching consequences for our code.
Let’s look at an example where we might want to use BigDecimal
: a Value Object to
store money. The most straightforward way in Kotlin might look like this:
import java.math.BigDecimal data class Money(val amount: BigDecimal) fun main() { val bad = Money(BigDecimal(0.1)) println(bad) val good = Money(0.1.toBigDecimal()) println(good) }
Notice that we are accepting BigDecimal
as a parameter. Hence, we are accepting an object that has
already been built—and we don’t control how. And as we know, there is a good and a bad way of
creating BigDecimal
. To prevent consumers of Money
from getting themselves into trouble, we should design
our code so it’s impossible to use a badly constructed BigDecimal
.
import java.math.BigDecimal class Money private constructor(val amount: BigDecimal) { constructor(amount: Double) : this(amount.toBigDecimal()) override fun toString() = "Money(amount=${amount})" } fun main() { val best = Money(0.1) println(best) }
In the updated class, we expect the caller to provide the amount as a Double
—never as a BigDecimal
.
Double
is then transformed to BigDecimal
in a controlled way and stored inside the Value Object. We store it as
BigDecimal
since we still prefer to do computations on BigDecimal
, not on a double
.
Also, we are removing data
from the class. Leaving data
would add a generated copy()
method that allows callers to
construct new instances with arbitrary values, bypassing the intended construction path.
TL;DR
BigDecimal
is safe for arithmetic operations, but the BigDecimal(double)
constructor behaves unpredictably.
Therefore, don’t accept BigDecimal
directly as a parameter to a Value Object. Accept primitives or strings and
construct the BigDecimal
yourself in a controlled way.
If you are using SonarQube, one of the default rules will check for any calls to the double‑argument constructor and highlight them as issues.