Implementing equals

All objects have both identity (the object's location in memory) and state (the object's data). The == operator always compares identity. The default implementation of equals compares identity as well.

Sometimes the default implementation of equals has the desired behaviour (as in a type-safe enumeration, for example), but equals should usually compare state, not identity. This is particularly true for "data-centric" classes which map to database records.

hashCode and equals are closely related:

Objects placed in a List, Set, or Map (as either a key or value) should have an appropriate definition of equals. (See, for example, the javadoc for Collection.contains , Map.containsKey, and Map.containsValue .)

If you extend a concrete class, and add a new field which contributes to equals, then it's not possible to write a perfectly correct equals method for the new class. Instead, you should use composition instead of inheritance. (See Effective Java by Joshua Bloch for more information.)

When implementing equals, fields are compared differently, according to their type:

It's worth noting that if fields are implemented with wrapper classes (Integer, Boolean, and so on), then implementation of equals is simpler, since there is only one case: calling the equals method recursively. (The compareTo method is also simplified in this case.)

In an equals method, it's usually worthwhile to order field comparisons such that the most significant comparisons are performed first. That is, fields most likely to differ should be evaluated first. This allows the && "short-circuit" logical operator to minimize execution time.

Example 1

The above policies can be collected in a utility class:


/**
* Collected methods which allow easy implementation of <code>equals</code>.
*
* Example use case in a class called Car:
* <pre>
public boolean equals(Object aThat){
  if ( this == aThat ) return true;
  if ( !(aThat instanceof Car) ) return false;
  Car that = (Car)aThat;
  return
    EqualsUtil.areEqual(this.fName, that.fName) &&
    EqualsUtil.areEqual(this.fNumDoors, that.fNumDoors) &&
    EqualsUtil.areEqual(this.fGasMileage, that.fGasMileage) &&
    EqualsUtil.areEqual(this.fColor, that.fColor) &&
    Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks); //array!
}
* </pre>
*
* <em>Arrays are not handled by this class</em>.
* This is because the <code>Arrays.equals</code> methods should be used for
* array fields.
*/
public final class EqualsUtil {

  static public boolean areEqual(boolean aThis, boolean aThat){
    //System.out.println("boolean");
    return aThis == aThat;
  }

  static public boolean areEqual(char aThis, char aThat){
    //System.out.println("char");
    return aThis == aThat;
  }

  static public boolean areEqual(long aThis, long aThat){
    /*
    * Implementation Note
    * Note that byte, short, and int are handled by this method, through
    * implicit conversion.
    */
    //System.out.println("long");
    return aThis == aThat;
  }

  static public boolean areEqual(float aThis, float aThat){
    //System.out.println("float");
    return Float.floatToIntBits(aThis) == Float.floatToIntBits(aThat);
  }

  static public boolean areEqual(double aThis, double aThat){
    //System.out.println("double");
    return Double.doubleToLongBits(aThis) == Double.doubleToLongBits(aThat);
  }

  /**
  * Possibly-null object field.
  *
  * Includes type-safe enumerations and collections, but does not include
  * arrays. See class comment.
  */
  static public boolean areEqual(Object aThis, Object aThat){
    //System.out.println("Object");
    return aThis == null ? aThat == null : aThis.equals(aThat);
  }
}
 


Car is a class which uses EqualsUtil to implement its equals method:

import java.util.*;

public final class Car {

  public Car (
    String aName, int aNumDoors, List<String> aOptions,
    double aGasMileage, String aColor, Date[] aMaintenanceChecks
  ){
    fName = aName;
    fNumDoors = aNumDoors;
    fOptions = new ArrayList<>(aOptions);
    fGasMileage = aGasMileage;
    fColor = aColor;
    fMaintenanceChecks = new Date[aMaintenanceChecks.length];
    for (int idx=0; idx < aMaintenanceChecks.length; ++idx) {
      fMaintenanceChecks[idx] = new Date( aMaintenanceChecks[idx].getTime() );
    }
  }

  @Override public boolean equals(Object aThat) {
    //check for self-comparison
    if ( this == aThat ) return true;

    //use instanceof instead of getClass here for two reasons
    //1. if need be, it can match any supertype, and not just one class;
    //2. it renders an explict check for "that == null" redundant, since
    //it does the check for null already - "null instanceof [type]" always
    //returns false. (See Effective Java by Joshua Bloch.)
    if ( !(aThat instanceof Car) ) return false;
    //Alternative to the above line :
    //if ( aThat == null || aThat.getClass() != this.getClass() ) return false;

    //cast to native object is now safe
    Car that = (Car)aThat;

    //now a proper field-by-field evaluation can be made
    return
      EqualsUtil.areEqual(this.fName, that.fName) &&
      EqualsUtil.areEqual(this.fNumDoors, that.fNumDoors) &&
      EqualsUtil.areEqual(this.fOptions, that.fOptions) &&
      EqualsUtil.areEqual(this.fGasMileage, that.fGasMileage) &&
      EqualsUtil.areEqual(this.fColor, that.fColor) &&
      Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks);
  }

  //..other methods elided

  // PRIVATE

  /**
  * The following fields are chosen to exercise most of the different
  * cases.
  */
  private String fName;
  private int fNumDoors;
  private List<String> fOptions;
  private double fGasMileage;
  private String fColor; //treat as possibly-null
  private Date[] fMaintenanceChecks;

  /**
  * Exercise the equals method.
  */
  public static void main (String... aArguments) {
    List<String> options = new ArrayList<String>();
    options.add("sunroof");
    Date[] dates = new Date[1];
    dates[0] = new Date();

    //Create a bunch of Cars; only one and two should be equal
    Car one = new Car("Nissan", 2, options, 46.3, "Green", dates);

    //two is equal to one
    Car two = new Car("Nissan", 2, options, 46.3, "Green", dates);

    //three has a differs in fName only
    Car three = new Car("Pontiac", 2, options, 46.3, "Green", dates);

    //four differs in fNumDoors only
    Car four = new Car("Nissan", 4, options, 46.3, "Green", dates);

    //five differs in fOptions only
    List<String> optionsTwo = new ArrayList<String>();
    optionsTwo.add("air conditioning");
    Car five = new Car("Nissan", 2, optionsTwo, 46.3, "Green", dates);

    //six differs in fGasMileage only
    Car six = new Car("Nissan", 2, options, 22.1, "Green", dates);

    //seven differs in fColor only
    Car seven = new Car("Nissan", 2, options, 46.3, "Fuchsia", dates);

    //eight differs in fMaintenanceChecks only
    Date[] datesTwo = new Date[1];
    datesTwo[0] = new Date(1000000);
    Car eight = new Car("Nissan", 2, options, 46.3, "Green", datesTwo);

    System.out.println( "one = one: " + one.equals(one) );
    System.out.println( "one = two: " + one.equals(two) );
    System.out.println( "two = one: " + two.equals(one) );
    System.out.println( "one = three: " + one.equals(three) );
    System.out.println( "one = four: " + one.equals(four) );
    System.out.println( "one = five: " + one.equals(five) );
    System.out.println( "one = six: " + one.equals(six) );
    System.out.println( "one = seven: " + one.equals(seven) );
    System.out.println( "one = eight: " + one.equals(eight) );
    System.out.println( "one = null: " + one.equals(null) );
  }
} 


An example run of this class demonstrates that only objects one and two are equal:
one = one: true
one = two: true
two = one: true
one = three: false
one = four: false
one = five: false
one = six: false
one = seven: false
one = eight: false
one = null: false

Example 2

The WEB4J tool has a utility class for implementing equals. The following is an example of a Model Object implemented with WEB4J.

Items to note regarding this equals method:



package hirondelle.fish.main.discussion; 

import java.util.*;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.security.SafeText;
import static hirondelle.web4j.util.Consts.FAILS;

/** 
 Comment posted by a possibly-anonymous user.
*/
public final class Comment { 

  /**
   Constructor. 
     
   @param aUserName identifies the logged in user posting the comment. 
   @param aBody the comment, must have content.
   @param aDate date and time when the message was posted.
  */
  public Comment (
    SafeText aUserName, SafeText aBody, Date aDate
  ) throws ModelCtorException {
    fUserName = aUserName;
    fBody = aBody;
    fDate = aDate.getTime();
    validateState();
  }

  /** Return the logged in user name passed to the constructor. */
  public SafeText getUserName() {
    return fUserName;
  }

  /** Return the body of the message passed to the constructor.  */
  public SafeText getBody() {
    return fBody;
  }

  /**
   Return a <a href="http://www.javapractices.com/Topic15.cjp">defensive copy</a> 
   of the date passed to the constructor.
   
   <P>The caller may change the state of the returned value, without affecting 
   the internals of this <tt>Comment</tt>. Such copying is needed since 
   a {@link Date} is a mutable object.
  */
  public Date getDate() {
    // the returned object is independent of fDate
    return new Date(fDate);
  }

  /** Intended for debugging only. */
  @Override public String toString() {
    return ModelUtil.toStringFor(this);
  }

  @Override public boolean equals( Object aThat ) {
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      Comment that = (Comment) aThat;
      result = ModelUtil.equalsFor(
        this.getSignificantFields(), that.getSignificantFields()
      );
    }
    return result;    
  }

  @Override public int hashCode() {
    if ( fHashCode == 0 ) {
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }

  // PRIVATE // 
  private final SafeText fUserName;
  private final SafeText fBody;
  /** Long is used here instead of Date in order to ensure immutability.*/
  private final long fDate;
  private int fHashCode;
  
  private Object[] getSignificantFields(){
    return new Object[] {fUserName, fBody, new Date(fDate)};
  }
  
  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    if( FAILS ==  Check.required(fUserName) ) {
      ex.add("User name must have content.");
    }
    if ( FAILS == Check.required(fBody) ) {
      ex.add("Comment body must have content.");
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
}
 



See Also :
Implementing compareTo
Implementing hashCode
Beware of instanceof operator
Multiple return statements
Use boxing with care
Would you use this technique?
Yes   No   Undecided   
© 2014 Hirondelle Systems | Source Code | Contact | License | RSS
Individual code snippets can be used under this BSD license - Last updated on September 21, 2013.
Over 2,000,000 unique IPs last year - Built with WEB4J.
- In Memoriam : Bill Dirani -