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
BigDecimalas 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
equalsandhashCodeis already easy enough to get wrong (option 2) and it is easy to forget and hard to validateBigDecimalfields have been normalised (option 3). More special handling inequalsandhashCodemoves 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@EqualsAndHashCodeor Java 16 (preview since 14)recordclasses: it is a shame having to add boilerplate for every@EqualsAndHashCodeannotated class or everyrecordclass that has aBigDecimalfield. -
Implement
equalsto usecompareToforBigDecimalfields and ensure values of those fields that are equal usingcompareTowill 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.equalsandObjects.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
BigDecimalfields 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 standardequalsandhashCodecan be used. For example, your class ensures all variants of1such as1.0,1.00, etc are converted to1in 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_EQUALITYin 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()).