25

I've read this article: How to Write an Equality Method in Java.

Basically, it provides a solution for an equals() method that supports inheritance:

Point2D twoD   = new Point2D(10, 20);
Point3D threeD = new Point3D(10, 20, 50);
twoD.equals(threeD); // true
threeD.equals(twoD); // true

But is it a good idea? these two instances appears to be equal but may have two different hash codes. Isn't that a bit wrong?

I believe this would be better achieved by casting the operands instead.

  • 1
    The example with colored points as given in the link makes more sense to me. I would consider that a 2D point (x,y) can be seen as a 3D point with a zero Z component (x,y,0), and I would like the equality to return false in your case. In fact, in the article, a ColoredPoint is explicitely said to be different from a Point and always return false. – coredump Oct 28 '15 at 14:12
  • 10
    Nothing worse than tutorials that break common conventions... It takes years to break those kinds of habits out of programmers. – corsiKa Oct 28 '15 at 19:43
  • 3
    @coredump Treating a 2D point as having a zero z coordinate might be a useful convention for some applications (Early CAD systems handling legacy data come to mind). But it's an arbitrary convention. Planes in spaces with 3 or more dimensions can have arbitrary orientations...it's what makes interesting problems interesting. – ben rudgers Oct 29 '15 at 3:57
  • 2
    It's more than a bit wrong. – Kevin Krumwiede Oct 29 '15 at 7:22
71

This should not be equality because it breaks transitivity. Consider these two expressions:

new Point3D(10, 20, 50).equals(new Point2D(10, 20)) // true
new Point2D(10, 20).equals(new Point3D(10, 20, 60)) // true

Since equality is transitive, this should mean that the following expression is also true:

new Point3D(10, 20, 50).equals(new Point3D(10, 20, 60))

But of course - it isn't.

So, your idea of casting is correct - expect that in Java, casting simply means casting the type of the reference. What you really want here is a conversion method that'll create a new Point2D object from a Point3D object. This would also make the expression more meaningful:

twoD.equals(threeD.projectXY())
  • 1
    The article describes implementations that break transitivity and offers a range of work-arounds. In a domain where we allow 2D points, we already have decided that the third dimension doesn't matter. and so (10, 20, 50) equals (10, 20, 60) is fine. We only care about 10 and 20. – ben rudgers Oct 28 '15 at 17:59
  • 1
    Should Point2D have a projectXYZ() method to provide a Point3D representation of itself? In other words, should implementations know each other? – h.j.k. Oct 29 '15 at 3:16
  • 4
    @h.j.k. Getting rid of Point2D seems simpler since projecting 2D points requires defining their plane in 3D space first. If the 2D point knows it's plane, then it's already a 3D point. If it doesn't, it can't project. I'm reminded of Abbott's Flatland. – ben rudgers Oct 29 '15 at 3:53
  • @benrudgers You can, though, define a Plane3D object, which will define a plane in 3D space, that plane can have a lift method(2D->3D is lifting, not projecting) that'll accept a Point2D and a number for the "third axis" - distance from the plane along the plane normal. For ease of usage, you can define the common planes as static constants, so you could do things like Plane3D.XY.lift(new Point2D(10, 20), 50).equals(new Point3D(10, 20, 50)) – Idan Arye Oct 29 '15 at 16:35
  • @IdanArye I was commenting on the suggestion that 2D points should have a projection method. As to planes with lift methods, I think it would require two arguments to make sense: a 2D point and the plane it is assumed to be on, i.e. it really needs to be a projection if it doesn't own the point...and if it owns the point, why not just own a 3D point and do away with a problematic data type and a the smell of a kludged method? YMMV. – ben rudgers Oct 29 '15 at 23:25
10

I walk away from reading the article thinking about the wisdom of Alan J. Perlis:

Epigram 9. It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.

The fact that getting "equality" right is the sort of problem that keeps Martin Ordersky inventor of Scala up at night should give pause about whether overriding equals in an inheritance tree is a good idea.

What happens when we're unlucky to get a ColoredPoint is that our geometry fails because we used inheritance to proliferate data types rather than making one good one. This despite having to go back and modify the root node of the inheritance tree to make equals work. Why not just add a z and a color to Point?

The good reason would that Point and ColoredPoint operate in different domains...at least if those domains never mingled. Yet if that's the case, we don't need to override equals. Comparing ColoredPoint and Point for equality only makes sense in a third domain where they're allowed to mingle. And in that case, it's probably better to have the "equality" tailored to that third domain rather than trying to apply equality semantics from one or the other or both of the unmingled domains. In other words "equality" should be defined local to the place where we've got mud flowing in from both sides because we may not want ColoredPoint.equals(pt) to fail against instances of Point even if the author of ColoredPoint thought it was a good idea six months ago at 2am.

6

When the old programming gods were inventing object-oriented programming with classes, they decided when it came to composition and inheritance to have two relationships for an object: "is a" and "has a".
This partially solved the problem of subclasses being different than parent classes but made them usable without breaking code. Because a subclass instance "is a" superclass object and can be substituted directly for it, even though the subclass has more member functions or data members, the "has a" guarantees it will perform all the functions of the parent and have all of its members. So you could say a Point3D "is a" Point , and a Point2D "is a" Point if they both inherit from Point. Additionally a Point3D could be a subclass of Point2D.

Equality between classes is problem domain-specific, however, and the above example is ambiguous as to what the programmer needs for the program to work correctly. Generally, math-domain rules are followed and values of data would generate equality if you limit the scope of the comparision to just in this case two dimensions, but not if you compare all the data members.

So you get a table of narrowing equalities:

Both objects have same values, limited to subset of shared members

Child classes can be equal to parent classes if parent and childs
data members are the same.

Both objects entire data members are the same.

Objects must have all same values and be similar classes. 

Objects must have all same values and be the same class type. 

Equality is determined by specific logical conditions in the domain.

Only Objects that both point to same instance are equal. 

You generally pick the most strict rules that you can that still will perform all the necessary functions in your problem domain. The built-in equality tests for numbers are designed to be as restrictive as they can be for math purposes, but the programmer has many ways around that if that is not the goal, including rounding up/down , truncation, gt,lt,etc. Objects with timestamps are often compared by their generation time and so each instance must be unique so comparisons get very specific.

The design factor in this case is to determine efficient ways to compare objects. Sometimes a recursive comparison of all objects data members is what you must do, and that can get very expensive if you have lots and lots of objects with lots of data members. Alternatives are to only compare relevant data values, or have the object generate a hash value of its data members concerned for a quick comparison with other similar objects, keep collections sorted and pruned to make comparisons faster and less cpu intensive, and perhaps allow objects that are identical in data to be culled and a duplicate pointer to a single object be put in its place.

2

The rule is, whenever you override hashcode(), you override equals(), and vice versa. Whether this is a good idea or not depends on the intended usage. Personally, I would go with a different method (isLike() or similar) to achieve the same effect.

  • 1
    It can be OK to override hashCode without overriding equals. For example, one would do that to test a different hashing algorithm for the same equality condition. – Patricia Shanahan Oct 28 '15 at 23:02
1

It is often useful for non-public-facing classes to have an equivalence-testing method which allows objects of different types to consider each other "equal" if they represent the same information, but because Java allows no means by which classes can impersonate each other it's often good to have a single public-facing wrapper type in cases where it might be possible to have equivalent objects with differing representations.

For example, consider a class encapsulating an immutable 2D matrix of double values. If one outside method asks for an identity matrix of size 1000, a second asks for a diagonal matrix and passes an array containing 1000 ones, and a third asks for a 2D matrix and passes a 1000x1000 array where elements on the primary diagonal are all 1.0 and all others are zero, the objects given to all three classes may use different backing stores internally [the first having a single field for size, the second having a thousand-element array, and the third having a thousand 1000-element arrays] but should report each other as equivalent [since all three encapsulate a 1000x1000 immutable matrix with ones on the diagonal and zeroes everywhere else].

Beyond the fact that it hides the existence of distinct backing-store types, the wrapper will also be useful for facilitating comparisons, since checking items for equivalence will generally be a multi-step process. Ask the first item if it knows whether it's equal to the second; if it doesn't know, ask the second if it knows whether it's equal to the first. If neither object knows, then ask each array about the contents of its individual elements [one might add other checks before deciding to do the long-slow individual-item comparison route].

Note that the equivalence-test method for each object in this scenario would need to return a three-state value ("Yes I'm equivalent", "No I'm not equivalent", or "I don't know"), so the normal "equals" method would not be suitable. While any object could simply reply "I don't know" when asked about any other, adding logic to e.g. a diagonal matrix which would not bother asking any identity matrix or diagonal matrix about any elements off the main diagonal would greatly expedite comparisons among such types.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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