BigDecimal equality
Be aware that BigDecimal
fields with values such as 1
, 1.0
, 1.00
, … are not considered equal.
The Comparable
interface strongly recommends but does not require that implementations consider two objects equal using
compareTo
whenever they are equal using equals
and vice versa. BigDecimal
is a class where this is not applied.
BigDecimal one = new BigDecimal("1");
BigDecimal alsoOne = new BigDecimal("1.0");
// prints true - 1 is the same as 1.0
System.out.println(one.compareTo(alsoOne) == 0);
// prints false - 1 is not the same as 1.0
System.out.println(one.equals(alsoOne));
Ways to resolve this error
If values like 1
and 1.0
need not be equal then this check can be disabled by suppressing Warning.BIGDECIMAL_EQUALITY
.
EqualsVerifier.forClass(Foo.class)
.suppress(Warning.BIGDECIMAL_EQUALITY)
.verify();
If values like 1
and 1.0
should be equal then some options are:
-
Do not use
BigDecimal
as fields. But unfortunately I cannot recommend a well-known generally accepted drop-in alternative.The argument for this: it is not great having to complicate classes with the options below. A valid
equals
andhashCode
is already easy enough to get wrong (option 2) and it is easy to forget and hard to validateBigDecimal
fields have been normalised (option 3). More special handling inequals
andhashCode
moves against standardisation, such as making things easier e.g.Objects.equals
,Objects.hashCode
, or eliminating the need to think about it e.g. Lombok’s@EqualsAndHashCode
or Java 16 (preview since 14)record
classes: it is a shame having to add boilerplate for every@EqualsAndHashCode
annotated class or everyrecord
class that has aBigDecimal
field. -
Implement
equals
to usecompareTo
forBigDecimal
fields and ensure values of those fields that are equal usingcompareTo
will produce the same hashcode.EqualsVerifier checks this by default which causes the BigDecimal equality error messages.
It wouldn’t be unwise to use utility methods as this is not as simple as a call to Java’s
Objects.equals
andObjects.hashcode
. The logic for correct equality is:// true if bdField and other.bdField are // either both null or are equal using compareTo boolean comparablyEqual = (bdField == null && other.bdField == null) || (bdField != null && other.bdField != null && bdField.compareTo(other.bdField) == 0);
A consistent hashcode needs a way to normalise the value that it represents. A simple normalisation is:
// Remove trailing zeros from the unscaled value of the // BigDecimal to yield a consistently scaled instance int consistentHashcode = Objects.hashCode(bdField.stripTrailingZeros());
-
Normalise the
BigDecimal
fields during your class’s construction (and setters if it is mutable) such that there is only one way it represents the same value for these fields. This means standardequals
andhashCode
can be used. For example, your class ensures all variants of1
such as1.0
,1.00
, etc are converted to1
in its constructor.A simple normalisation is:
class Foo { ... Foo(BigDecimal bdField) { // Now if bdField is final it will only have instances // that are equal when compareTo is equal. // If mutable then setters will need to do the same. this.bdField = bdField.stripTrailingZeros(); } }
Unfortunately it is imporssible for EqualsVerifier to confirm this has been done. Therefore, you still need to suppress
Warning.BIGDECIMAL_EQUALITY
in this case.
If performance is important then you will want to consider the costs of using BigDecimal
and of where and how normalisation
is achieved. Option 2 performs the work when objects are stored in a HashSet
or used as keys in a HashMap
. Option 3
performs work on creation of each object but is then cheaper to hash. There may be better normalisations than stripTrailingZeros
.
BigDecimal
already has a cost traded in return for its accuracy.
Why does this happen for BigDecimal?
BigDecimal
can have multiple representations of the same value. It uses an unscaled value and a scale (both integers).
For example, the value of 1 can be represented as unscaled value 1 with scale of 0 (scale is the number of places after
the decimal point when 0 or greater) or as unscaled value 10 with scale of 1 resolving to 1.0. Its equals
and hashCode
methods use both of these attributes in their calculation rather than the resolved value.
There is more information on compareTo
and equals
in the Comparable
Javadoc and Effective Java’s chapter on implementing Comparable
.
There is more information on BigDecimal
in its Javadoc (and its representation can be seen by printing unscaledValue()
and scale()
).