It never amazes me how hard equals() is! After some study, I see the problem. Fortunately, there is a solution...
If you think in terms of the table-per-class inheritance model, I want the discriminator column in the equivalence relation which comparing a fixed projection of the table onto a set of columns. Equality after projection is always an equivalence relation, so this must work. In my Bug, SubBug example this set of fields is {discriminator, name, subname}.
The way hibernate implements equals uses instanceof instead of getClass() comparison.. This is precisely the difference between including or not including the discriminator column in the table projection. Interestingly, eclipse generates equals() methods that always use the getClass() comparison. Neither is right or wrong, they simply define different equalivalence relations. Depending on what I'm modelling I will need both at different times.
The example Bug.equals method from hibernate:
Code:
public boolean equals(Object other) {
if ( (this == other ) ) return true;
if ( (other == null ) ) return false;
if ( !(other instanceof Bug) ) return false;
Bug castOther = ( Bug ) other;
return ( (this.getName()==castOther.getName()) || ( this.getName()!=null && castOther.getName()!=null && this.getName().equals(castOther.getName()) ) );
Compare to what eclipse does via Source->Generate hashcode() and Equals() for Bug.equals :
Code:
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Bug other = (Bug) obj;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
and then SubBug.equals
Code:
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!super.equals(obj))
return false;
if (getClass() != obj.getClass())
return false;
final SubBug other = (SubBug) obj;
if (subname == null) {
if (other.subname != null)
return false;
} else if (!subname.equals(other.subname))
return false;
return true;
}
Note the gotcha with the eclipse way is that even if I cast I can never have a Bug equal a SubBug.
Code:
Bug bug1 = new Bug("null pointer");
SubBug subBug2 = new SubBug("null pointer", "foo");
Bug bug2 = (Bug)subBug2;
assert (bug2.equals(bug1) == false);
The last assertion is because bug2.equals calls SubBug.equals (not Bug.equals!) and the getClass() returns SubBug, while obj.getClass() returns Bug, so it drops out. This seems really strange unless you think about it in terms of the database table and projecting onto {discriminator, name, subname}. Casting doesn't change the getClass() value, nor the discriminator column.
If you don't include the discriminator column in the projection, but do use a column that only exists in a subtype, you get into trouble when you cast up, because the parent class can't do the projection you really want as it doesn't have access to all the fields. The child can, so you get different results. Hence the violation of symmetry.
The implementation mechanism to allow both seems obvious: allow use-in-equals to apply to the descriminator too. Then use getClass comparison like eclipse if it's set. Otherwise use instanceof as you already do.[/code]