DEV Community

On the subtleties of OOP

Martin Häusler on November 09, 2017

It is not a overstatement to say that I am an enthusiast when it comes to object orientation. A well crafted object oriented system is a piece of a...
Collapse
 
carlfish profile image
Charles Miller • Edited

A painful lesson that every Java developer learns is that subclassing and equality are a dangerous mix.

One defensive measure is not to use 'instanceof' in non-final equals method, because any use of instanceof will be asymmetric if subclassed. You can enforce symmetry by doing an exact class comparison —this.getClass() == other.getClass() — instead, which is what most tool-generated equals methods end up doing. This code also acts as a clear warning to developers: "equality does not survive subclassing".

However, this still leads to the same problem that broke your program, because two things that are otherwise indistinguishable when treated as type A (and thus should be equal) remain not equal to each other, because one is secretly of type B.

The fault ultimately lies in Java's concept of universal equality; the idea that there is a coherent concept of equality that transcends the types of the things being compared. The problem is that there is no such thing as coherent universal equality (for the reasons we've discovered above), but it's often really convenient to pretend there is. It's one of those places in Java where simplicity of interface was favoured over correctness of implementation, leaving behind a lot of pitfalls for developers.

Outside of the enforced requirement of universal equality, there's no real reason that Pair<A, B> shouldn't be a supertype of Triple<A, B, C>. Every sensible operation you can perform on a pair can be unambiguously projected onto a triple, so the latter should be substitutable for the former.

A fix for this would be to introduce a concept of equality in which the meaning of equality depends on the type of the comparison (similar to how you can supply a comparator when sorting). You could then express concepts like "A equals B as pairs", "A equals B as triples" or "A equals B as object references" distinctly and unambiguously.

Collapse
 
martinhaeusler profile image
Martin Häusler • Edited

First of all, thanks for the elaborate comment!

I totally agree that universal equality is difficult (if not impossible) to truly achieve. In my opinion, the Java designers did the best they could without totally going overboard with it. It is a huge improvement over what other languages do, Javascript for example has no real notion of equality other than reference equality (plus, interestingly, specific equalities for built-in types such as strings). But that is another matter entirely.

I think that, strictly speaking, a mathematician would disagree with the statement that a triple "is" a pair. However, a triple can be projected to a pair.

The best solution in this case is the one you outlined. Have Pair and Triple as independent classes (no inheritance) and let Triple have a method asPair that projects the triple to its first two entries, producing a new Pair. Also, Pair could have a toTriple(C third) method. This is also the mathematically correct way of doing it. Doing an "implicit projection" of the triple to the first two entries is questionable practice at best and dangerous at worst (as my example above has shown).

Oh, and Object#equals(...) is only getting started here. Things can get really hairy really quickly if we also add JDK Dynamic Proxies into the mix. The example I presented is really just the tip of the iceberg. My intention was to highlight how important it is to read the JavaDoc before implementing a method, and to focus on actual semantics rather than mere syntax.