Implementing equals
Immutable objects
Lazy initialization
Prefer Collections over older classes
Consider wrapper classes for optional data
hashCode
:
equals
, it must override hashCode
equals
and hashCode
must use the same set of fieldshashCode
values must be equal as wellhashCode
is a candidate for caching and
lazy initializationhashCode
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); } }
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:
hashCode
value is calculated only once, and only if it is needed. This is only possible since this is
an immutable object.
getSignificantFields()
method ensures hashCode
and equals
remain 'in sync'
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; } }