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.
First of all, thanks for the elaborate comment!
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.
We're a place where coders share, stay up-to-date and grow their careers.
We strive for transparency and don't collect excess data.