2

Consider a class with a comparable (consistent with equals) and a non-comparable field (of a class about which I do not know whether it overrides Object#equals or not).

The class' instances shall be compared, where the resulting order shall be consistent with equals, i.e. 0 returned iff both fields are equal (as per Object#equals) and consistent with the order of the comparable field. I used System.identityHashCode to cover most of the cases not covered by these requirements (the order of instances with same comparable, but different other value is arbitrary), but am not sure whether this is the best approach.

public class MyClass implements Comparable<MyClass> {
    private Integer intField;
    private Object nonCompField;

    public int compareTo(MyClass other) {
        int intFieldComp = this.intField.compareTo(other.intField);
        if (intFieldComp != 0)
            return intFieldComp;
        if (this.nonCompField.equals(other.nonCompField))
            return 0;
        // ...and now? My current approach:
        if (Systems.identityHashCode(this.nonCompField) < Systems.identityHashCode(other.nonCompField))
            return -1;
        else
            return 1;
     }
}

Two problems I see here:

  • If Systems.identityHashCode is the same for two objects, each is greater than the other. (Can this happen at all?)
  • The order of instances with same intField value and different nonCompField values need not be consistent between runs of the program, as far as I understand what Systems.identityHashCode does.

Is that correct? Are there more problems? Most importantly, is there a way around this?

arne.b
  • 4,212
  • 2
  • 25
  • 44
  • Why do you want to compare such an object, and why do you want the comparison to include the explicitly non-comparable field? – Louis Wasserman Sep 20 '12 at 17:08
  • @LouisWasserman To use it in a data structure that relies on sorting internally, like a `TreeMap`, for example. – arne.b Sep 20 '12 at 18:51
  • That doesn't really address the question. Why would you want such a thing to be e.g. in a `TreeMap`? Can you explain what you're _actually_ trying to do in the real world that motivated this question? – Louis Wasserman Sep 20 '12 at 19:56
  • @LouisWasserman I came across a class in a larger code base that had a comparator that compared numerical fields and returned 1 if these were equal and a non-comparable field was not. I tried to fix it, but realized that even my above approach did not work as that class was used in a sorting data structure similar to a `TreeSet`, but in a way that in fact relied on the violation of symmetry of the comparator. I remebered having used the above approach somewhere else before, but now think I could also use a (sorted) `List` and a `HashMap` together instead of one TreeMap there. – arne.b Sep 21 '12 at 08:31

3 Answers3

2

The first problem, although highly unlikely, could happen (I think you would need an enormous amount of memory, and a very bad luck). But it's solved by Guava's Ordering.arbitrary(), which uses the identity hash code behind the scenes, but maintains a cache of comparison results for the cases where two different objects have the same identity hash code.

Regarding your second question, no, the identity hash codes are not preserved between runs.

JB Nizet
  • 678,734
  • 91
  • 1,224
  • 1,255
1

Systems.identityHashCode […] the same for two objects […] (Can this happen at all?)

Yes it can. Quoting from the Java API Documentation:

As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects.
identityHashCode(Object x) returns the same hash code for the given object as would be returned by the default method hashCode(), whether or not the given object's class overrides hashCode().

So you may encounter hash collisions, and with memory ever growing but hash codes staying fixed at 32 bit, they will become increasingly more likely.

The order of instances with same intField value and different nonCompField values need not be consistent between runs of the program, as far as I understand what Systems.identityHashCode does.

Right. It might even be different during a single invocation of the same program: You could have (1,foo) < (1,bar) < (1,baz) even though foo.equals(baz).

Most importantly, is there a way around this?

You can maintain a map which maps each distinct value of the non-comparable type to a sequence number which you increase for each distinct value you encounter.

Memory management will be tricky, though: You cannot use a WeakHashMap as the code might make your key object unreachable but still hold a reference to another object of the same value. So either you maintain a list of weak references to all the objects of a given value, or you simply use strong references and accept the fact that any uncomparable value ever encountered will never be garbage collected.

Note that this scheme will still not result in reproducible sequence numbers unless you create values reproducibly in just the same order.

MvG
  • 57,380
  • 22
  • 148
  • 276
  • I realize now I was in fact thinking about symmetry and not transitivity when I wrote the question title. Very good point about "`(1,foo) < (1,bar) < (1,baz)` even though `foo.equals(baz)`". – arne.b Sep 21 '12 at 08:32
1

If the class of the nonCompField has implemented a reasonably good toString(), you might be able to use

return String.valueOf(this.nonCompField).compareTo(String.valueOf(other.nonCompField));

Unfortunately, the default Object.toString() uses the hashcode, which has potential issues as noted by others.

user949300
  • 15,364
  • 7
  • 35
  • 66