The Contract for Equality


The Contract for Equality

The Java API documentation for the equals method in Object provides a list of what defines an equivalence relation between two objects:

Reflexivity:

x.equals(x)

Symmetry:

x.equals(y) if-and-only-if (iff) y.equals(x)

Transitivity:

if x.equals(y) and y.equals(z) , then x.equals(z)

Consistency:

x.equals(y) returns a consistent value given consistent state

Comparison to null:

! x.equals(null)


The above contract should hold true for all non- null values of x and y . You can implement each of these rules in your unit test for equality. [6]

[6] A nice set of JUnit extensions, located at http:// sourceforge .net/projects/junit-addons, provides an automated means of testing the equality contract.

public void testEquality() {
   Course courseA = new Course("NURS", "201");
   Course courseAPrime = new Course("NURS", "201");
   assertEquals(courseA, courseAPrime);

   Course courseB = new Course("ARTH", "330");
   assertFalse(courseA.equals(courseB));

// reflexivity


assertEquals(courseA, courseA);


// transitivity


Course courseAPrime2 = new Course("NURS", "201");


assertEquals(courseAPrime, courseAPrime2);


assertEquals(courseA, courseAPrime2);


// symmetry


assertEquals(courseAPrime, courseA);


// consistency


assertEquals(courseA, courseAPrime);


// comparison to null


assertFalse(courseA.equals(null));

}

When you run your tests, everything will pass but the final assertion in testEquality . You will receive a NullPointerException: The that argument is null and the equals code attempts to access its fields ( department and object ). You can add a guard clause to return false if the argument is null :

@Override
public boolean equals(Object object) {

if (object == null)


return false;

Course that = (Course)object;
   return
      this.department.equals(that.department) &&
      this.number.equals(that.number);
}

You might choose to represent each of the "qualities of equality" (reflexivity, transitivity, symmetry, consistency, and comparison to null) in a separate test. There are differing views on approaches to TDD. Some developers feel that you should structure your code so that each test contains a single assertion. [7] I take the viewpoint that the goal of a test is to prove a single piece of functionality. Doing so may require many assertions/postconditions. In this example, the many assertions you just coded represent a complete contract for a single piece of functionalityequality.

[7] [Astels2004].


Apples and Oranges

Since the equals method takes an Object as parameter, you can pass anything to it (and the code will still compile). Your equals method should handle this situation:

// apples & oranges
assertFalse(courseA.equals("CMSC-120"));

Even though the String value matches the department and number of courseA , a Course is not a String. You would expect this comparison to return false . When Java executes the equals method, however, you receive a ClassCastException. The line of the equals method that executes the cast is the cause:

@Override
public boolean equals(Object object) {
   if (object == null)
      return false;

Course that = (Course)object;

return
      this.department.equals(that.department) &&
      this.number.equals(that.number);
}

The code attempts to cast the String parameter to a Course reference. This invalid cast generates the ClassCastException.

You need a guard clause that immediately returns false if someone throws an orange at your equals method. The guard clause ensures that the type of the parameter matches the type of the receiver.

@Override
public boolean equals(Object object) {
   if (object == null)
      return false;

if (this.getClass() != object.getClass())


return false;

Course that = (Course)object;
   return
      this.department.equals(that.department) &&
      this.number.equals(that.number);
}

You learned about the getClass method in Lesson 8. It returns a class constant. For the example comparison that threw the ClassCastException, the class constant of the receiver is Course.class , and the class constant of the parameter is String.class . Class constants are uniquethere is only one "instance" of the Class object Course.class . This uniqueness allows you to compare class constants using the operator != (not equal to) instead of comparing them using equals .

Some developers choose to use the instanceof operator. The instanceof operator returns true if an object is an instance of, or subclass of, the target class. Here is the equals method rewritten to use instanceof .

@Override
public boolean equals(Object object) {
   if (object == null)
      return false;

if (!(object instanceof Course))


return false;

Course that = (Course)object;
   return
      this.department.equals(that.department) &&
      this.number.equals(that.number);
}

You should initially prefer to compare the classes of the receiver and argument, instead of using instanceof . The class comparison only returns true if both objects are of the exact same type. Introduce instanceof later if you need to compare objects irrespective of their position in an inheritance hierarchy. See Lesson 12 for more information on instanceof .

JUnit and Equality

Sometimes I will code equals tests to be a bit more explicit. Instead of saying:

assertEquals(courseA, courseB);

I'll express the comparison as:

assertTrue(courseA.equals(courseB));

You should be clear on the fact that assertEquals uses the equals method for reference comparisons. But it can be helpful to explicitly emphasize that fact, and show that the equals method is what you're actually testing.

You may want to compare two references to see if they are the same. In other words, do they point to the same object in memory? You could code this comparison as:

assertTrue(courseA == courseB);

or as:

assertSame(courseA, courseB);

You should prefer the second assertion form, since it supplies a better error message upon failure. As before, though, you might choose to explicitly show the == comparison if for purposes of building an equals method test.