For those who DONT know why equals() is really complicated, scroll to the end for an explanation. Without knowing about it this post is probably not going to make much sense. If you understand why a hypothetical "ColoredList extends ArrayList" class, which adds a color property to any list, MUST have an equals implementation that says that a red empty list is equal to a blue empty list, even though that seems silly, you don't need to read the footnote.
What we really need is for AbstractList's equals() method to be intelligent enough to realize if 'other' is a subclass of AbstractList that isn't adding any state that is relevant for equality, in which case it can do its comparison as usual, or, if 'other' is a subclass that DOES add state relevant for equality, such as a color property. If that is the case, AbstractList's equals method should conclude immediately with: Not equal, even if the contents are. A few people have proposed such a system, including a somewhat well known writeup by Venners and Odersky. It's very long so I'll explain the gist here, but the full paper can be found here: http://www.artima.com/lejava/articles/equality.html What they propose is adding a protected boolean canEquals(Object o) method. The equals() method will actually call other.canEquals(this), and if that is false, return false. The standard implementation of any canEquals method pretty much always looks like: return (o instanceof Point3D);, where Point3D is replaced with the closest parent (or yourself) that added equality-significant state. Thus, ArrayList and LinkedList would not override AbstractList's canEquals (which has: return (o instanceof AbstractList);), but something like a ColoredList WOULD override and replace it with "return (o instanceof ColoredList)". This works.... provided you don't forget to override the canEquals() method, which, as its certainly not a standard java idiom is easy to forget, and it also introduces another method to the API. My flash of insight here is to use this trick to entirely avoid the need for a canEquals method *AND* automatically do the right thing, leaving virtually no room for accidental error: if (!(o instanceof Self)) return false; Method m1 = o.getClass().getMethod("equals", Object.class); Method m2 = Self.class.getMethod("equals", Object.class); if (m1 != m2) return false; The idea is: If a hypothetical other.equals(this) call would end up using the same equals method as myself, then these objects could be equal, even if their actual types don't match. A new equivalence relation, like Point3D or colouredlist, HAVE to override equals so they can include their new property (z for Point3D, colour for ColouredList) in the comparison. However, an implementation detail, such as ArrayList and LinkedList, or a JPA proxy, have absolutely no need for overriding AbstractList/Point's equals method, and in fact, they don't. I've double-checked the java sources, neither LinkedList nor ArrayList override AbstractList's default equals implementation. I guess there's a somewhat theoretical space where a subclass overrides equals() for efficiency reasons, but that's probably an acceptable price to pay to gain the advantage of not having another method cluttering up the API, and a far smaller chance of breaking the contract by forgetting to override canEquals. Am I missing something, or is this too hacky a solution? FOOTNOTE: Why is equals problematic? Equality in java is a lot more problematic than you might at first glance think. Josh Bloch, when he wrote effective java, proposed the following template for writing equals methods. Let's assume we have a simple point class: public boolean equals(Object o) { if (o == null) return false; if (o == this) return true; if (!(o instanceof Point)) return false; if (((Point)o).x != this.x) return false; if (((Point)o).y != this.y) return false; return true; } Simple enough. But wrong. In the second edition, the instanceof check was revised to this: if (o.getClass() != this.getClass()) return false; and the reason is the equals contract, which says that equality in java must be reflexive (if a.equals(b), then b.equals(a) must also hold), symmetric (a.equals(a) must always hold) and transitive (if a.equals(b), and b.equals(c), then a.equals(c) must hold). symmetric is simple enough, but the others aren't. Let's say there's a subclass of Point named 3d point, which adds a z coordinate. Equals is easily rewritten to include: if (((Point3D)o).z != this.z) return false; - but what should Point3D do when you give it a Point class? There's only one thing to do, because of the reflexive rule: It should compare x and y and not compare z (as the non-3D point has none). It HAS to do this - because when calling point2d.equals(point3d), that's what happens, and you have to do the same as it. But now we're in deep trouble. If [0, 0, 1] is equal to [0, 0], and [0, 0] is in turn equal to [0, 0, 2], we are forced by the transitivity rule to conclude that [0, 0, 1] is equal to [0, 0, 2]. But that's preposterous! Nobody would expect these 2 different points in 3D space to nevertheless be .equals() to each other. And yet, that's the ONLY way to get equality right if Point is written with that instanceof check. This is why Josh revised effective java. But now we have a problem: Technically, one should only use subclassing when changing the nature of objects. i.e. you have a class named "Shape" and you subclass it to create "Square". It's perfectly allright than any random shape is never equal to a square, but unfortunately there's a lot of subclassing merely for implementation details. For example, LinkedList and ArrayList are virtual similes of each other and certainly model the same construct, they are just different implementations. AbstractList's equals() method is basically broken because a LinkedList can be equals to an ArrayList - it uses the instanceof style. As long as you only create implementations which don't add new state of their own, you're fine, but if you ever create a ColoredList class, which gives all lists color, you MUST write its equals method so that an empty red list is equal to an empty blue list, even though that seems ridiculous. After all, you can't change ArrayList's equals() method, and it will ignore that color property. Then, by way of transitivity, red lists equal blue lists if their contents are equal. Your hands are tied. -- You received this message because you are subscribed to the Google Groups "The Java Posse" group. To post to this group, send email to [email protected]. To unsubscribe from this group, send email to [email protected]. For more options, visit this group at http://groups.google.com/group/javaposse?hl=en.
