views:

421

answers:

4

I believe this question applies equally well to C# as to Java, because both require that {c,C}ompareTo be consistent with {e,E}quals:

Suppose I want my equals() method to be the same as a reference check, i.e.:

public bool equals(Object o) {
    return this == o;
}

In that case, how do I implement compareTo(Object o) (or its generic equivalent)? Part of it is easy, but I'm not sure about the other part:

public int compareTo(Object o) {
    MyClass other = (MyClass)o;
    if (this == other) {
        return 0;
    } else {
        int c = foo.CompareTo(other.foo)
        if (c == 0) {
            // what here?
        } else {
            return c;
        }
    }
}

I can't just blindly return 1 or -1, because the solution should adhere to the normal requirements of compareTo. I can check all the instance fields, but if they are all equal, I'd still like compareTo to return a value other than 0. It should be true that a.compareTo(b) == -(b.compareTo(a)), and the ordering should stay consistent as long as the objects' state doesn't change.

I don't care about ordering across invocations of the virtual machine, however. This makes me think that I could use something like memory address, if I could get at it. Then again, maybe that won't work, because the Garbage Collector could decide to move my objects around.

hashCode is another idea, but I'd like something that will be always unique, not just mostly unique.

Any ideas?

A: 

The generic equivalent is easier to deal with in my opinion, depends what your external requirements are, this is a IComparable<MyClass> example:

public int CompareTo(MyClass other) {
    if (other == null) return 1;
    if (this == other) {
        return 0;
    } else {
        return foo.CompareTo(other.foo);
    }
}

If the classes are equal or if foo is equal, that's the end of the comparison, unless there's something secondary to sort on, in that case add it as the return if foo.CompareTo(other.foo) == 0

If your classes have an ID or something, then compare on that as secondary, otherwise don't worry about it...the collection they're stored it and it's order in arriving at these classes to compare is what's going to determine the final order in the case of equal objects or equal object.foo values.

Nick Craver
+3  A: 

First of all, if you are using Java 5 or above, you should implement Comparable<MyClass> rather than the plain old Comparable, therefore your compareTo method should take parameters of type MyClass, notObject:

public int compareTo(MyClass other) {
    if (this == other) {
        return 0;
    } else {
        int c = foo.CompareTo(other.foo)
        if (c == 0) {
            // what here?
        } else {
            return c;
        }
    }
}

As of your question, Josh Bloch in Effective Java (Chapter 3, Item 12) says:

The implementor must ensure sgn(x.compareTo(y)) == -sgn(y.compare- To(x)) for all x and y. (This implies that x.compareTo(y) must throw an exception if and only if y.compareTo(x) throws an exception.)

This means that if c == 0 in the above code, you must return 0.

That in turn means that you can have objects A and B, which are not equal, but their comparison returns 0. What does Mr. Bloch have to say about this?

It is strongly recommended, but not strictly required, that (x.compareTo(y) == 0) == (x.equals(y)). Generally speaking, any class that implements the Comparable interface and violates this condition should clearly indicate this fact. The recommended language is “Note: This class has a natural ordering that is inconsistent with equals.”

And

A class whose compareTo method imposes an order that is inconsistent with equals will still work, but sorted collections containing elements of the class may not obey the general contract of the appropriate collection interfaces (Collection, Set, or Map). This is because the general contracts for these interfaces are defined in terms of the equals method, but sorted collections use the equality test imposed by compareTo in place of equals. It is not a catastrophe if this happens, but it’s something to be aware of.

Update: So IMHO with your current class, you can not make compareTo consistent with equals. If you really need to have this, the only way I see would be to introduce a new member, which would give a strict natural ordering to your class. Then in case all the meaningful fields of the two objects compare to 0, you could still decide the order of the two based on their special order values.

This extra member may be an instance counter, or a creation timestamp. Or, you could try using a UUID.

Péter Török
My question is precisely How do I make compareTo consistent with equals when equals just tests for reference equality?
Paul A Jungwirth
@Paul A Jungwirth OK, I get it :-) Pls see my update.
Péter Török
Hmm, the counter and unique ID are both possible ideas. Do you know if C# has something comparable to Java's unique ID?
Paul A Jungwirth
@Paul A Jungwirth Unfortunately I am not quite familiar with C# :-(
Péter Török
After some Googling, I actually don't see any way to get a unique object ID in Java. Some people wrongly suggest using identityHashCode, but that is not guaranteed to be unique. That means I'd have to go with setting a field from a static counter, which in a multi-threaded environment would have to be synchronized. . . . And it looks like Ruben makes all these points!
Paul A Jungwirth
@Paul A Jungwirth Yes, his post is excellent, deserved a +1 :-) I was indeed wrong with the toString thing, I fixed my answer. I still have this vague memory though about something I must have read in SO some weeks ago, I just can't find it.
Péter Török
@Paul Now I found it, it is in fact the JDWP object ID, which seems to be not applicable for you :-(
Péter Török
+1  A: 

Two objects don't need to be reference equal to be in the same equivalence class. In my opinion, it should be perfectly acceptable for two different objects to be the same for a comparison, but not reference equal. It seems perfectly natural to me, for example, that if you hydrated two different objects from the same row in the database, that they would be the same for comparison purposes, but not reference equal.

I'd actually be more inclined to modify the behavior of equals to reflect how they are compared rather than the other way around. For most purposes that I can think of this would be more natural.

tvanfosson
+2  A: 

In Java or C#, generally speaking, there is no fixed ordering of objects. Instances can be moved around by the garbage collector while executing your compareTo, or the sort operation that's using your compareTo.

As you stated, hash codes are generally not unique, so they're not usable (two different instances with the same hash code bring you back to the original question). And the Java Object.toString implementation which many people believe to surface an object id (MyObject@33c0d9d), is nothing more than the object's class name followed by the hash code. As far as I know, neither the JVM nor the CLR have a notion of an instance id.

If you really want a consistent ordering of your classes, you could try using an incrementing number for each new instance you create. Mind you, incrementing this counter must be thread safe, so it's going to be relatively expensive (in C# you could use Interlocked.Increment).

Ruben