Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Implementing hashCode

Implementing hashCode: It's a popular misconception that hashCode provides a unique identifier for an object. It does not.

Example 1

The simplest case for implementing hashCode (and equals) 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

The following utility class allows simple construction of an effective hashCode method. It's based on the recommendations of Effective Java, by Joshua Bloch.

import java.lang.reflect.Array;

/**
* Collected methods which allow easy implementation of <tt>hashCode</tt>.
*
* Example use case:
* <pre>
*  public int hashCode(){
*    int result = HashCodeUtil.SEED;
*    //collect the contributions of various fields
*    result = HashCodeUtil.hash(result, fPrimitive);
*    result = HashCodeUtil.hash(result, fObject);
*    result = HashCodeUtil.hash(result, fArray);
*    return result;
*  }
* </pre>
*/
public final class HashCodeUtil {

  /**
  * An initial value for a <tt>hashCode</tt>, to which is added contributions
  * from fields. Using a non-zero value decreases collisons of <tt>hashCode</tt>
  * values.
  */
  public static final int SEED = 23;

  /** booleans.  */
  public static int hash(int aSeed, boolean aBoolean) {
    log("boolean...");
    return firstTerm( aSeed ) + (aBoolean ? 1 : 0);
  }

  /*** chars.  */
  public static int hash(int aSeed, char aChar) {
    log("char...");
    return firstTerm(aSeed) + (int)aChar;
  }

  /** ints.  */
  public static int hash(int aSeed , int aInt) {
    /*
    * Implementation Note
    * Note that byte and short are handled by this method, through
    * implicit conversion.
    */
    log("int...");
    return firstTerm(aSeed) + aInt;
  }

  /** longs.  */
  public static int hash(int aSeed , long aLong) {
    log("long...");
    return firstTerm(aSeed)  + (int)(aLong ^ (aLong >>> 32));
  }

  /** floats.  */
  public static int hash(int aSeed , float aFloat) {
    return hash(aSeed, Float.floatToIntBits(aFloat));
  }

  /** doubles. */
  public static int hash(int aSeed , double aDouble) {
    return hash( aSeed, Double.doubleToLongBits(aDouble) );
  }

  /**
  * <tt>aObject</tt> is a possibly-null object field, and possibly an array.
  *
  * If <tt>aObject</tt> is an array, then each element may be a primitive
  * or a possibly-null object.
  */
  public static int hash(int aSeed , Object aObject) {
    int result = aSeed;
    if (aObject == null){
      result = hash(result, 0);
    }
    else if (!isArray(aObject)){
      result = hash(result, aObject.hashCode());
    }
    else {
      int length = Array.getLength(aObject);
      for (int idx = 0; idx < length; ++idx) {
        Object item = Array.get(aObject, idx);
        //if an item in the array references the array itself, prevent infinite looping
        if(! (item == aObject))  
          //recursive call!
          result = hash(result, item);
        }
    }
    return result;
  }  
  
  // PRIVATE 
  private static final int fODD_PRIME_NUMBER = 37;

  private static int firstTerm(int aSeed){
    return fODD_PRIME_NUMBER * aSeed;
  }

  private static boolean isArray(Object aObject){
    return aObject.getClass().isArray();
  }
  
  private static void log(String aMessage){
    System.out.println(aMessage);
  }
} 

Here's an example of its use. When the logging statements are uncommented in HashCodeUtil, the reuse of the boolean, char, int and long versions of hash is demonstrated:
 

boolean...
char...
int...
long...
long...
int...
int...
int...
int...
int...
hashCode value: -608077094

import java.util.*;

public final class ApartmentBuilding {

  public ApartmentBuilding(
    boolean aIsDecrepit,
    char aRating,
    int aNumApartments,
    long aNumTenants,
    double aPowerUsage,
    float aWaterUsage,
    byte aNumFloors,
    String aName,
    List<String> aOptions,
    Date[] aMaintenanceChecks
  ){
    fIsDecrepit = aIsDecrepit;
    fRating = aRating;
    fNumApartments = aNumApartments;
    fNumTenants = aNumTenants;
    fPowerUsage = aPowerUsage;
    fWaterUsage = aWaterUsage;
    fNumFloors = aNumFloors;
    fName = aName;
    fOptions = aOptions;
    fMaintenanceChecks = aMaintenanceChecks;
  }

  @Override public boolean equals(Object that) {
    if (this == that) return true;
    if (!(that instanceof ApartmentBuilding)) return false;
    ApartmentBuilding thatBuilding = (ApartmentBuilding)that;
    return hasEqualState(thatBuilding);
  }

  @Override public int hashCode() {
    //this style of lazy initialization is 
    //suitable only if the object is immutable
    if (fHashCode == 0) {
      int result = HashCodeUtil.SEED;
      result = HashCodeUtil.hash(result, fIsDecrepit);
      result = HashCodeUtil.hash(result, fRating);
      result = HashCodeUtil.hash(result, fNumApartments);
      result = HashCodeUtil.hash(result, fNumTenants);
      result = HashCodeUtil.hash(result, fPowerUsage);
      result = HashCodeUtil.hash(result, fWaterUsage);
      result = HashCodeUtil.hash(result, fNumFloors);
      result = HashCodeUtil.hash(result, fName);
      result = HashCodeUtil.hash(result, fOptions);
      result = HashCodeUtil.hash(result, fMaintenanceChecks);
      fHashCode = result;
    }
    return fHashCode;
  }

  //..elided..

  // PRIVATE

  /**
  * The following fields are chosen to exercise most of the different
  * cases.
  */
  private boolean fIsDecrepit;
  private char fRating;
  private int fNumApartments;
  private long fNumTenants;
  private double fPowerUsage;
  private float fWaterUsage;
  private byte fNumFloors;
  private String fName; //possibly null, say
  private List<String> fOptions; //never null
  private Date[] fMaintenanceChecks; //never null
  private int fHashCode;

  /**
  * Here, for two ApartmentBuildings to be equal, all fields must be equal.
  */
  private boolean hasEqualState(ApartmentBuilding that) {
    //note the different treatment for possibly-null fields
    return
      ( this.fName==null ? that.fName==null : this.fName.equals(that.fName) ) &&
      ( this.fIsDecrepit == that.fIsDecrepit )&&
      ( this.fRating == that.fRating )&&
      ( this.fNumApartments == that.fNumApartments ) &&
      ( this.fNumTenants == that.fNumTenants ) &&
      ( this.fPowerUsage == that.fPowerUsage ) &&
      ( this.fWaterUsage ==  that.fWaterUsage ) &&
      ( this.fNumFloors == that.fNumFloors ) &&
      ( this.fOptions.equals(that.fOptions) )&&
      ( Arrays.equals(this.fMaintenanceChecks, that.fMaintenanceChecks) )
    ;
  }

  /** Exercise hashcode.  */
  public static void main (String [] aArguments) {
    List<String> options = new ArrayList<>();
    options.add("pool");
    Date[] maintenanceDates = new Date[1];
    maintenanceDates[0] = new Date();
    byte numFloors = 8;

    ApartmentBuilding building = new ApartmentBuilding (
      false, 'B', 12, 396L,
      5.2,  6.3f, numFloors,  "Palisades",
      options, maintenanceDates
    );

    System.out.println("hashCode value: " + building.hashCode());
  }
} 

Example 3

The WEB4J tool defines a utility class for implementing hashCode. Here is an example of a Model Object implemented with that utility.

Items to note:


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 equals
Immutable objects
Lazy initialization
Prefer Collections over older classes
Consider wrapper classes for optional data