5

Java documentation says it's "strongly recommended" to have them behaving consistently.

But are there legitimate cases of java/c#/python/etc Object.equals() method behaving inconsistently with the method Comparable.compareTo()?

6
  • 2
    You wish your coworkers to strangle you to death? I can think of no good scenario.
    – Telastyn
    May 5, 2015 at 23:04
  • lol. i've just been told that BigDecimal has this peculiarity (which kinda makes sense)
    – Wes
    May 5, 2015 at 23:18
  • 1
    Why does it make sense for BigDecimal?
    – kdgregory
    May 6, 2015 at 0:37
  • 7
    @kdgregory BigDecimal makes a distinction between 1.00 and 1.0000 as different. So while they sort to the same location (compareTo), they have a different internal state and are not equal. You could also think of a rational class where 1/3 and 2/6 have the same value but different state. The confusing part would be that equal in doesn't imply mathematical equality but rather object equality (which is where it gets confusing when dealing with mathlike objects).
    – user40980
    May 6, 2015 at 22:27
  • 1
    Thank you very much, @MichaelT. It's nice to see that an experienced programmer can answer that question. I wrote the comment, however, in the hope of getting the OP to think about the differences.
    – kdgregory
    May 7, 2015 at 10:48

4 Answers 4

15

The reason you have two different methods is that they do two different things.

The .equals method returns a boolean value indicating whether the object on which you call the method is equal to the object passed in as a parameter (for some definition of "is equal to" that is consistent with the nature of the object being compared).

The .compareTo method returns a negative integer, zero, or a positive integer as this object is less than, equal to, or greater than the specified object. That makes it a useful method for sorting; it allows you to compare one instance with another for purposes of ordering.

When the Java documentation says that these two methods must behave consistently, what they mean is that the .equals method must return true in exactly the same situations where the .compareTo method returns zero, and must return false in exactly the same situations where the .compareTo method returns a nonzero number.

Is there any good reason to violate these rules? Generally no, for the same reasons that

#define TRUE FALSE

is a really bad idea. The only legitimate reason for inconsistent behavior is hinted at in the Java documentation itself: "[if the] class has a natural ordering that is inconsistent with equals."

To drive home the point, you can actually define .equals() in terms of compareTo(), thus guaranteeing consistent behavior. Consider this .equals() method from a Rational class which, after a few sanity checks, simply defines .equals as compareTo() == 0:

public boolean equals(Object y) {
    if (y == null) return false;
    if (y.getClass() != this.getClass()) return false;
    Rational b = (Rational) y;
    return compareTo(b) == 0;
}
1
  • I've also seen it the other way round, where CompareTo()'s zero stars is defined in terms of equals, but this makes much more sense now that I've seen it done this way.
    – RubberDuck
    May 7, 2015 at 21:06
7

This confusion would occur in situations where there are conflicting understandings of equals.

compareTo is 'easy' in that it asks a simple question 'which one is bigger?' If you were given a bunch of Things, how do you sort them?

equals on the other hand wants to ask 'are these the same thing?'

BigDecimal in Java is one such place where there are conflicting understandings of what it means to have two things being equal.

BigDecimal foo = new BigDecimal("1.00");
BigDecimal bar = new BigDecimal("1.000");

These two entires will have the same meaning for sorting. They, however, are not the same when it comes to equality. They have a different underlying state (the precision of the number) and that means they are not equal but foo.compareTo(bar) will return 0.

Consider this, if you had a Map qux = HashMap<BigDecimal, Object>() for some reason, do you want qux.put(foo,foo) to take the same spot as qux.put(bar,bar) and thus evict the earlier insertion?

So, while they are math equals (which is how compareTo sorts them), they are not inner state equals, and thus the necessity of the inconsistency here.

Yes, this inconsistency comes at the price of a higher cognitive load for dealing with BigDecimal. It means maps may not behave like you want them to... and the question is "which map do you want to behave 'right'?"

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.TreeMap;

class Main {
    public static void main (String[] args) {
        BigDecimal foo = new BigDecimal("1.00");
        BigDecimal bar = new BigDecimal("1.000");

        HashMap<BigDecimal, String> hash = new HashMap();
        TreeMap<BigDecimal, String> tree = new TreeMap();

        hash.put(foo, "foo");
        hash.put(bar, "bar");

        tree.put(foo, "foo");
        tree.put(bar, "bar");

        System.out.println("hash foo: " + hash.get(foo));
        System.out.println("hash bar: " + hash.get(bar));

        System.out.println("tree foo: " + tree.get(foo));
        System.out.println("tree bar: " + tree.get(bar));
    }
}

ideone

Output:

hash foo: foo
hash bar: bar
tree foo: bar
tree bar: bar

Because compareTo returned 0, in the TreeMap, bar evicted foo when bar was inserted. However, because these are different objects with different internal state and thus different hash codes, they were both able to exist within a HashMap.

From the docs:

Note: care should be exercised if BigDecimal objects are used as keys in a SortedMap or elements in a SortedSet since BigDecimal's natural ordering is inconsistent with equals. See Comparable, SortedMap or SortedSet for more information.

And so, thats the problem, the inconsistency and the "there's no good answer to this" dilemma.


One could also imagine this with a Rational class where one wants to keep 2/4 having the internal state of 2/4 so that:

Rational twoFourths = new Rational(2,4);
Rational oneHalf = new Rational(1,2);

System.out.println(twoFourths); // prints 2/4
System.out.println(oneHalf);    // prints 1/2
System.out.println(twoFourths.compareTo(oneHalf)); // prints 0
System.out.println(twoFourths.equals(oneHalf));    // ???

And you are once again up against that same question. Are these equal with the mathematical sense of equality (which means the hashCode needs to also return the same value?) or are they not equal using the object oriented state of the object sense of equality despite being the 'same'.

2

The "legitimate reason" for having equals() inconsistent with compareTo() is if they serve different real-world purposes.

Let's start with BigDecimal. If you're simply sorting a list of values, you probably don't care about scale. However, if you're checking for equality, what you're really checking is that the series of operations that led to this result are the same. To be honest, I'm surprised that the developers of BigDecimal did not choose to include rounding mode in their test for equality.

As another example, consider customer address entities stored in a database. You get cheaper postal rates if you sort addresses for mass mailings, so you might choose to base the natural ordering on the postal code and street address. However, even if two addresses are identical in all outward respects, they may not be equal, because a particular address belongs to a particular customer (and you'll get into all sorts of issues if you try to share them).

Those are two cases where equality requires a stronger guarantee than ordering. The reverse is possible although unlikely: I once implemented a system where I needed to ensure stable ordering of different instances, so added an additional test in comparison, such that different instances would never compare as 0. But I can't remember the details of that system, and can't come up with a good example on the spur of the moment.

As other people have said, you have to be careful when you use objects where equality and order are inconsistent. Personally, I prefer to implement a Comparator rather than have the classes be Comparable.

2

But are there legitimate cases of java/c#/python/etc Object.equals() method behaving inconsistently with the method Comparable.compareTo()?

Yes, for example, here is a legitimate case in Python 2.x. The float value for NaN does not compare equal (==) to itself, yet cmp() (equivalent to compareTo) will return 0 (equal), which is inconsistent with the result of ==:

>>> x = float('nan')
>>> x == x
False
>>> cmp(x, x)
0

(Note: cmp() was removed in Python 3; that's why I specified Python 2.x only.)

NaN is kind of a special case in most languages, because IEEE 754 defines NaN as not equal to itself, something which is not true for basically any other value encountered in programming.

Interestingly, Java and C# do not have this inconsistency. Even though NaN in the primitive type double is not equal to itself using the primitive equality operator, the equals method of Double objects will test NaN equal to itself, to preserve consistency with compareTo and to allow use as keys in dictionaries. So they opted to make primitive equality and equals inconsistent, rather than make equals and compareTo inconssitent.

Ruby also does not have this inconsistency. NaN compared to itself with the <=> operator returns nil (incomparable). So they avoided the inconsistency by restricting comparability to a subset of numbers.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.