equals() vs compareTo()
Objective
equals() and compareTo() both answer "are these two objects the same?", but they come from different places and say different things. equals(Object) is declared on Object, so every Java object has it, and it returns a boolean: equal or not. compareTo(T) is declared on the Comparable<T> interface, so only types that opt in have it, and it returns an int: negative if this is less than the other, zero if they are equal, positive if this is greater. The short answer is "one returns a boolean, the other an int". The longer answer is that the two are expected to agree on what "equal" means, and when they don't (as with BigDecimal), collections built on one of them behave differently from collections built on the other.
Use Cases
- Answering the interview question "what is the difference between
equals()andcompareTo()?" with more than the return types. - Deciding whether a class needs only
equals()/hashCode()(it goes in aHashSetor is aHashMapkey) or also a natural ordering viaComparable(it gets sorted, or goes in aTreeSet/TreeMap). - Comparing money amounts held in
BigDecimal, wherenew BigDecimal("1.0").equals(new BigDecimal("1.00"))isfalse. - Explaining why the same values produce a set of size 2 in a
HashSetand size 1 in aTreeSet.
Deep Dive
Where each method comes from
equals() is inherited by every class from java.lang.Object. The default implementation is identity (this == obj); classes that represent values override it.
compareTo() only exists if the class implements java.lang.Comparable<T>. Many JDK types do: String, every primitive wrapper (Integer, Long, Double, Character, ...), BigDecimal, BigInteger, LocalDate, enums, and so on. Your own classes can implement it too.
javapublic record Version(int major, int minor) implements Comparable<Version> {
@Override
public int compareTo(Version other) {
int byMajor = Integer.compare(major, other.major);
return byMajor != 0 ? byMajor : Integer.compare(minor, other.minor);
}
}The record gets equals() for free from its components; the ordering is something it had to declare explicitly.
What each one returns
equals() gives a yes/no answer. compareTo() gives a direction, which is exactly what sorting algorithms need:
java"apple".equals("banana"); // false
"apple".compareTo("banana"); // -1 (negative: "apple" comes first)
"b".compareTo("a"); // 1 (positive: "b" comes after)
"a".compareTo("a"); // 0 (equal)
Integer.valueOf(5).compareTo(7); // -1Only the sign is part of the contract. String.compareTo() can return -1, -17 or -3000; callers must test < 0, == 0, > 0, never == -1.
They also differ on null
equals(null) is required to return false. compareTo(null) is required to throw, because there is no meaningful "direction" to null:
java"a".equals(null); // false
"a".compareTo(null); // NullPointerExceptionConsistent with equals
The Comparable documentation asks for this rule: x.compareTo(y) == 0 should have the same truth value as x.equals(y). Two objects that are equal should compare as zero, and two objects that compare as zero should be equal. The JDK calls an ordering that satisfies this consistent with equals. It is strongly recommended, not enforced, and that gap is where the bugs come from.
The reason it matters is that the JDK's collections pick one of the two methods and ignore the other:
HashSet,HashMap,List.contains(),List.indexOf()useequals()(andhashCode()).TreeSet,TreeMap,Collections.binarySearch()usecompareTo()(or aComparator) and never callequals().
If the two methods disagree, the same data behaves differently depending on which collection holds it.
BigDecimal: the famous inconsistency
A BigDecimal is an unscaled integer plus a scale. 1.0 is 10 × 10^-1 and 1.00 is 100 × 10^-2. equals() compares value and scale; compareTo() compares only the numeric value:
javaBigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");
a.equals(b); // false (different scale)
a.compareTo(b); // 0 (same numeric value)Put the same two objects into a hash-based and a tree-based collection and you get two different answers:
javanew HashSet<>(List.of(a, b)).size(); // 2 (uses equals)
new TreeSet<>(List.of(a, b)).size(); // 1 (uses compareTo)
Map<BigDecimal, String> hash = new HashMap<>();
hash.put(a, "x");
hash.get(b); // null
Map<BigDecimal, String> tree = new TreeMap<>();
tree.put(a, "x");
tree.get(b); // "x"For numeric comparison of BigDecimal, use compareTo() == 0. If you need values like 1.0 and 1.00 to be interchangeable as hash keys, normalize them first:
javaa.stripTrailingZeros().equals(b.stripTrailingZeros()); // trueDouble: consistent, but not with ==
Double.equals() and Double.compareTo() agree with each other, but both disagree with the primitive == operator on two edge cases:
javaDouble.valueOf(0.0).equals(-0.0); // false
Double.valueOf(0.0).compareTo(-0.0); // 1
0.0 == -0.0; // true
Double.valueOf(Double.NaN).equals(Double.NaN); // true
Double.valueOf(Double.NaN).compareTo(Double.NaN); // 0
Double.NaN == Double.NaN; // falseThe wrapper methods deliberately define a total order (needed for sorting and for NaN keys to be findable), while == follows IEEE 754.
Trade-offs
- An ordering inconsistent with equals is legal and compiles silently, but sorted collections then break the
Set/Mapcontract.Setis specified in terms ofequals(), yet aTreeSetdecides membership withcompareTo(), so it can reject an element thatequals()says is new.javaSet<String> names = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); names.add("Java"); names.add("JAVA"); // returns false: "JAVA".equals("Java") is false, names.size(); // 1 but the comparator says 0 - Implementing
Comparablecommits a type to one natural ordering. Any other ordering has to live in aComparator; trying to fit two orderings into onecompareTo()fights the contract. This is a design decision rather than something to demonstrate in code. compareTo()written as subtraction is shorter but overflows.a - bonly has the right sign when the difference fits in anint;Integer.compare()is always correct.javaint a = Integer.MIN_VALUE, b = 1; a - b; // 2147483647 (positive: wrong) Integer.compare(a, b); // -1 (negative: right)- For
BigDecimal, choosingequals()orcompareTo()is choosing whether scale matters. Scale is real information (10.00says "to the cent",10does not), soequals()is not wrong, just stricter than most business code expects.javanew BigDecimal("10").equals(new BigDecimal("10.00")); // false new BigDecimal("10").compareTo(new BigDecimal("10.00")) == 0; // true