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 .)

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.

If you extend a concrete class, and add a new field which contributes to equals, then you have to be careful (as shown below in Example 3).

Example 1

The simplest case for implementing equals (and hashCode) is to not use primitive fields or array fields. This is usually not an onerous restriction, because:


import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 With respect to implementing equals and hashCode, the simplest case is to 
 simply never use primitive fields or array fields.
 
 If a significant field is an array, then equals and hashCode need to process each 
 element of the array. For this reason, it's simpler to use a Collection 
 instead of an array.
*/
public final class ApartmentComplex {

  /** Possible features of an apartment building. */
  enum Feature {
    AIR_COND, CENTRAL_VAC, INDOOR_PARKING
  }
  
  /**
   Constructor.
    
   @param name possibly empty
   @param isDecrepit never null
   @param options never null, possibly empty
   @param maintenanceChecks never null, possibly empty
   @param features never null, possibly empty; callers typically use EnumSet.of(). 
   @param numApartments nullable
   @param numTenants nullable
  */
  public ApartmentComplex(
    String name,
    Boolean isDecrepit,
    Set<String> options,
    List<LocalDate> maintenanceChecks,
    Set<Feature> features,
    Integer numApartments,
    Long numTenants
  ){
    this.name = Objects.requireNonNull(name);
    this.isDecrepit = Objects.requireNonNull(isDecrepit);
    this.options = Objects.requireNonNull(options);
    this.maintenanceChecks = Objects.requireNonNull(maintenanceChecks);
    this.features = Objects.requireNonNull(features);
    this.numApartments = numApartments;
    this.numTenants = numTenants;
  }

  @Override public boolean equals(Object aThat) {
    //a standard implementation pattern
    //only the type 'ApartmentComplex' changes
    if (this == aThat) return true;
    if (!(aThat instanceof ApartmentComplex)) return false;
    ApartmentComplex that = (ApartmentComplex)aThat;
    for(int i = 0; i < this.getSigFields().length; ++i){
      if (!Objects.equals(this.getSigFields()[i], that.getSigFields()[i])){
        return false;
      }
    }
    return true;
  }

  @Override public int hashCode() {
    //simple one-line implementation
    return Objects.hash(getSigFields());
  }
  
  /** For debugging only. */
  @Override public String toString(){
    return "'" + name + "'";
  }

  //..elided..

  // PRIVATE

  //no primitives, and no arrays!
  private String name; 
  private Boolean isDecrepit; 
  private Set<String> options; 
  private List<LocalDate> maintenanceChecks; 
  private Set<Feature> features;
  private Integer numApartments; 
  private Long numTenants;

  /** 
   Must be called by BOTH equals and hashCode.
   'sig' as in 'significant'.
   
   It helps a bit to put the most significant items first, such that 
   the equals method can return as soon as possible, by finding 
   unequal items rapidly. 
  */
  private Object[] getSigFields(){
    Object[] result = {
      name, isDecrepit, options, maintenanceChecks, 
      features, numApartments, numTenants
    };
    return result;
  }
} 

Example 2

If you're using primitive or array fields, then you need an alternate implementation. The policies mentioned above 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.name, that.name) &&
    EqualsUtil.areEqual(this.numDoors, that.numDoors) &&
    EqualsUtil.areEqual(this.gasMileage, that.gasMileage) &&
    EqualsUtil.areEqual(this.color, that.color) &&
    Arrays.equals(this.maintenanceChecks, that.maintenanceChecks); //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 a, boolean b){
    //System.out.println("boolean");
    return a == b;
  }

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

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

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

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

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

Car is a class which uses EqualsUtil to implement its equals method:
import java.util.*;

public final class Car {

  public Car (
    String name, int numDoors, List<String> options,
    double gasMileage, String color, Date[] maintenanceChecks
  ){
    this.name = name;
    this.numDoors = numDoors;
    this.options = new ArrayList<>(options);
    this.gasMileage = gasMileage;
    this.color = color;
    this.maintenanceChecks = new Date[maintenanceChecks.length];
    for (int idx=0; idx < maintenanceChecks.length; ++idx) {
      this.maintenanceChecks[idx] = new Date(maintenanceChecks[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.name, that.name) &&
      EqualsUtil.areEqual(this.numDoors, that.numDoors) &&
      EqualsUtil.areEqual(this.options, that.options) &&
      EqualsUtil.areEqual(this.gasMileage, that.gasMileage) &&
      EqualsUtil.areEqual(this.color, that.color) &&
      Arrays.equals(this.maintenanceChecks, that.maintenanceChecks);
  }

  //..other methods elided

  // PRIVATE

  /**
  * The types of these fields are chosen to exercise most of the different
  * cases.
  */
  private String name;
  private int numDoors;
  private List<String> options;
  private double gasMileage;
  private String color; //treat as possibly-null
  /** New code should use LocalDate! */
  private Date[] maintenanceChecks;

  /**
  * Exercise the equals method.
  */
  public static void main (String args) {
    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);

    log("one = one: " + one.equals(one));
    log("one = two: " + one.equals(two));
    log("two = one: " + two.equals(one));
    log("one = three: " + one.equals(three));
    log("one = four: " + one.equals(four));
    log("one = five: " + one.equals(five));
    log("one = six: " + one.equals(six));
    log("one = seven: " + one.equals(seven));
    log("one = eight: " + one.equals(eight));
    log("one = null: " + one.equals(null));
  }
  
  private static void log(String text) {
    System.out.println(text);
  }
} 

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 3

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;
  }
}
 

Extends

What happens when you extend a concrete class, and add a new significant field? According to Effective Java by Joshua Bloch, it's not possible to write a perfectly correct equals method for the new class. Instead, you should use composition instead of inheritance.

But, according to this article (Odersky, Spoon, and Venners), there's a simple technique for this case. It uses a new method canEqual, which defines, for the subclasses, what types should be considered as part of the equals method.

Example 4

import java.math.BigDecimal;

public class Star {

  public Star(String name, BigDecimal radius){
    this.name = name;
    this.radius = radius;
  }
  
  public final String getName(){return name;}
  public final BigDecimal getRadius() {return radius;}
  
  @Override public boolean equals(Object aThat){
    if (this == aThat) return true;
    if ( !(aThat instanceof Star) ) return false;
    Star that = (Star)aThat;
    return
      that.canEqual(this) &&
      EqualsUtil.areEqual(this.name, that.name) &&
      EqualsUtil.areEqual(this.radius, that.radius)
    ;
  }
  
  @Override public int hashCode(){
    //this style of lazy initialization is 
    //suitable only if the object is immutable
    if (hashCode == 0) {
      int result = HashCodeUtil.SEED;
      result = HashCodeUtil.hash(result, name);
      result = HashCodeUtil.hash(result, radius);
      hashCode = result;
    }
    return hashCode;    
  }
  
  /** This method is overridable. */
  public boolean canEqual(Object that) {
      return (that instanceof Star);
  }  
  
  // PRIVATE
  private String name;
  private BigDecimal radius;
  private int hashCode;
}
 

import java.math.BigDecimal;

public final class Pulsar extends Star {
  
  public Pulsar(String name, BigDecimal radius, BigDecimal radioPeak){
    super(name, radius);
    peakRadioFreq = radioPeak;
  }
  
  public final BigDecimal getPeakRadioFrequency(){
    return peakRadioFreq;
  }

  @Override public boolean equals(Object aThat){
    if (this == aThat) return true;
    if ( !(aThat instanceof Pulsar) ) return false;
    Pulsar that = (Pulsar)aThat;
    return
      that.canEqual(this) &&
      EqualsUtil.areEqual(this.peakRadioFreq, that.peakRadioFreq) &&
      super.equals(that)
    ;
  }
  
  @Override public int hashCode(){
    //this style of lazy initialization is 
    //suitable only if the object is immutable
    if (hashCode == 0) {
      int result = HashCodeUtil.SEED;
      result = HashCodeUtil.hash(result, peakRadioFreq);
      result = HashCodeUtil.hash(result, super.hashCode());
      hashCode = result;
    }
    return hashCode;    
  }
  
  @Override public boolean canEqual(Object that){
    return (that instanceof Pulsar);
  }

  // PRIVATE 
  
  /** Peak radio frequency, in Hertz. */
  private BigDecimal peakRadioFreq;
  private int hashCode;
}
 

See Also :
Implementing compareTo
Implementing hashCode
Beware of instanceof operator
Multiple return statements
Use boxing with care
Prefer Collections over older classes
Consider wrapper classes for optional data