Transitivity – Object Comparison
Transitivity
If two classes, A and B, have a bilateral agreement on their objects being equal, then this rule guarantees that one of them, say B, does not enter into an agreement with a third class C on its own. All classes involved must multilaterally abide by the terms of the contract.
A typical pitfall resulting in broken transitivity is when the equals() method in a subclass calls the equals() method of its superclass, as part of its equals comparison. The equals() method in the subclass can be implemented by code equivalent to the following line:
return super.equals(argumentObj) && compareSubclassSpecificAspects();
The idea is to compare only the subclass-specific aspects in the subclass equals() method and to use the superclass equals() method for comparing the superclass-specific aspects. However, this approach should be used with extreme caution. The problem lies in getting the equivalence contract fulfilled bilaterally between the superclass and the subclass equals() methods. Symmetry or transitivity can easily be broken.
If the superclass is abstract, using the superclass equals() method works well. There are no superclass objects for the subclass equals() method to consider. In addition, the superclass equals() method cannot be called directly by any clients other than subclasses. The subclass equals() method then has control of how the superclass equals() method is called. It can safely call the superclass equals() method to compare the superclass-specific aspects of subclass objects.
Consistency
This rule enforces that two objects that are equal (or non-equal) remain equal (or non-equal) as long as they are not modified. For mutable objects, the result of the equals comparison can change if one (or both) are modified between method invocations. However, for immutable objects, the result must always be the same. The equals() method should take into consideration whether the class implements immutable objects, and ensure that the consistency rule is not violated.
null Comparison This rule states that no object is equal to null. The contract calls for the equals() method to return false. The method must not throw an exception; that would be violating the contract. A check for this rule is necessary in the implementation. Typically, the argument object is explicitly compared with the null value:
if (argumentObj == null)
return false;
In many cases, it is preferable to use the instanceof pattern match operator. It determines whether the argument object is of the appropriate type and introduces a local variable of the appropriate subtype that denotes the argument object. It is always false if its left operand is null:
if (!(argumentObj instanceof MyRefType other))
return false;
// Local variable other can be used in further implementation.
Note that if the instanceof pattern match operator is true, the if condition is false so that the local variable other of MyRefType is introduced into the scope after the if statement. In fact, the if statement can often be eliminated if the fields are compared individually:
return (this == argumentObj) // Reflexivity test
|| (argumentObj instanceof MyRefType other // null comparison & cast
&& /* Can use variable other to compare individual fields. */);
The return statement above will now also return false if the instanceof pattern match operator determines that argumentObj is null or if it is not of MyRefType. If the instanceof pattern match operator returns true, the reference other can be safely used to compare the individual fields by short-circuit evaluation of the && operator (see Example 14.3).
Example 14.3 Implementing the equals() Method
Click here to view code image import java.util.Objects;
// Overrides equals(), but not hashCode().
public class UsableVNO {
private int release;
private int revision;
private int patch;
public UsableVNO(int release, int revision, int patch) {
this.release = release;
this.revision = revision;
this.patch = patch;
}
public int getRelease() { return this.release; }
public int getRevision() { return this.revision; }
public int getPatch() { return this.patch; }
@Override public String toString() {
return “(” + release + “.” + revision + “.” + patch + “)”;
}
@Override public boolean equals(Object obj) { // (1)
return (this == obj) // (2)
|| (obj instanceof UsableVNO vno // (3)
&& this.patch == vno.patch // (4)
&& this.revision == vno.revision
&& this.release == vno.release);
}
}