BigDecimal.equals compares both numeric value and scale, while compareTo compares numeric value without requiring matching scale.
Java BigDecimal equality: value and scale are separate contracts
Choose one identity policy
A charge of 47.0 and a charge of 47.00 compare as the same amount, but equals says they are different objects by value representation. HashSet follows equals and stores both; TreeSet uses natural ordering and keeps one. If the same business key passes through both structures, two counts can disagree even though neither implementation is broken.
For a fixed two-decimal currency boundary, normalize exact input to scale two before using the amount as a key. setScale(2, UNNECESSARY) rejects an input that would need rounding. For tax or exchange-rate calculations, choose and document a separate rounding rule; quietly rounding at an identity boundary can merge values the business treats as distinct.
Keep representation visible
stripTrailingZeros changes representation and can produce a negative scale for values with trailing powers of ten. It is useful when numeric equivalence is the domain rule, but it is not a blanket money formatter. Decimal arithmetic covers construction and division; sorted-set collisions explains why comparator equality controls membership.
The program starts from strings. Constructing BigDecimal from a binary floating-point double would import that double's approximation before the scale policy has a chance to act.
Working program
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class ChargeIdentityPolicy {
public static void main(String[] args) {
BigDecimal firstCharge = new BigDecimal("47.0");
BigDecimal secondCharge = new BigDecimal("47.00");
Set<BigDecimal> exactRepresentations = new HashSet<>();
Set<BigDecimal> numericOrder = new TreeSet<>();
exactRepresentations.add(firstCharge);
exactRepresentations.add(secondCharge);
numericOrder.add(firstCharge);
numericOrder.add(secondCharge);
Set<BigDecimal> fixedCurrency = new HashSet<>();
fixedCurrency.add(firstCharge.setScale(2, RoundingMode.UNNECESSARY));
fixedCurrency.add(secondCharge.setScale(2, RoundingMode.UNNECESSARY));
System.out.println(exactRepresentations.size() + "/" + numericOrder.size());
System.out.println(fixedCurrency.size());
}
}Output
2/1
1Cost and ownership
Hash-based membership is expected O(1) per operation; ordered-tree membership is O(log n). Normalization allocates a new decimal representation when needed. The fixed-scale rule belongs at the input boundary so later collections do not guess what equality means.
Common Mistakes
- Do not assume compareTo returning zero implies equals returns true.
- Do not feed binary double literals into exact decimal identity rules.
- Do not use stripTrailingZeros as an unexplained currency display policy.
Read next
Java BigDecimal: decimal amounts and explicit rounding, Java TreeSet comparator collisions: compare zero means one slot, Java HashSet: deduplication and equality, Java comparators: tie-breakers, overflow and sorted-key identity.
More numeric representation boundaries
Continue with Java RoundingMode.UNNECESSARY: reject hidden fractional cents.
