Monday, March 9, 2009

Using @Deprecated more liberally

I recently created a class and an interface for managing dates as just a month and year (ignoring the day) and called them YearMonth and YearMonthInterface. I then added comparison methods to YearMonth: greaterThan(YearMonthInterface x), lessThan(YearMonthInterface x), and equals(YearMonthInterface x). Do you see the problem?

equals() overrides Object.equals() and has a special significance for the Java Collections framework. According to the Java SE 6 spec, equals() must be reflexive, symmetric, transitive, and consistent, and x.equals(null) should return false. I had no way of knowing that any class that implemented the YearMonthInterface could satisfy those conditions. In fact, I already had one implementing class that could never satisfy symmetry: myYearMonth.equals(myOtherClass) would often not equal myOtherClass.equals(myYearMonth) because my other class was dependent on another object and could only be equal when its associated objects were also equal.

I decided to keep the YearMonth.equals(Object o) method as overriding Object.equals() so that YearMonth would work correctly with collections, but create a new method, YearMonth.equalTo(YearMonthInterface x), for manual comparisons. My concern was that I never wanted to mistakenly use equals() instead of equalTo(). The solution? Deprecate YearMonth.equals() and provide a comment suggesting the use of equalTo() instead. The compiler will now catch any attempt to use the wrong method and the collections framework can still use the equals() method without problem.

/**

This is deprecated because it does not compare YearMonthInterface objects,
rather it overrides Object's equals method. It still works in
collections, but there should be no reason to ever call it directly.
Use equalTo() instead which really compares two YearMonthInterfaces as
you would expect, but doesn't need to be symmetric.

@param other another YearMonth (or it will return false).
@return true of the YearMonths are equal.
*/
@Deprecated
@Override
public boolean equals(Object other) {
// Check address and null first for speedy return
if (this == other) {
return true;
}
if (other == null) {
return false;
}
if ( !(other instanceof YearMonth) ) {
return false;
}
final YearMonth that = (YearMonth) other;
return (this.year == that.year) && (this.month == that.month);
}

public boolean equalTo(YearMonthInterface that) {
// Check address and null first for speedy return
if (this == that) {
return true;
}
if (that == null) {
return false;
}
return (this.year == that.getYear()) && (this.month == that.getMonth());
}

This technique would not work if I were to release this class to the public, but for a utility class with limited application, it can prevent me and my coworkers from making a simple but costly mistake.

For a full explanation of how to write a good equals() method, see Effective Java Second Edition by Joshua Bloch Chapter 3 (pp 35-36 is specifically about symmetry). I'm about half way through this book and really enjoying it.

No comments: