hashCode and equals – why implementing them is important?

Every Java object implements equals() and hashCode() methods. Default implementations, inherited from the Object class, rely on the internal address of an instance in the virtual machine. In some scenarios forgetting to override them can produce memory leaks – here we would like to show you an example of such situation.

Example

Let’s take a look at the following design.

Java hashCode and equals - why implementing them is important

Java hashCode and equals – why implementing them is important

A Catalog is an object managing a collection of items. Every Item has a unique identifier composed of several values. The Catalog internally stores its Items in a hashed collection which has identifiers as keys and their corresponding items as values. To learn more about HashMap in Java watch our previous tutorial – Java HashMap

Of course, in a real application the Catalog service will have many additional functionalities. Here, for the sake of simplicity, we provide only necessary methods — that is why in the example Catalog serves as a wrapper for a hashed collection.

Catalog.java

import java.util.HashMap;

/**
 * Catalog class: storing a collection of Items and providing an interface for
 * adding, removing, obtaining and checking an Item's presence.
 */
public class Catalog {

	// Items are stored in a hashed collection.
	private HashMap<Identifier, Item> items = new HashMap<Identifier, Item>();

	public boolean isPresent(Identifier id) {
		return items.containsKey(id);
	}

	public void addItem(Item item) {
		this.items.put(item.getId(), item);
	}

	public Item getItem(Identifier id) {
		return items.get(id);
	}

	public void remove(Identifier id) {
		items.remove(id);
	}
}

Item.java

/**
 * Items stored in the Catalog. Each instance is bind with a unique identifier.
 */
public class Item {

	private final Identifier id;
	private final String name;

	public Item(Identifier id, String name) {
		this.id = id;
		this.name = name;
	}

	public Identifier getId() {
		return id;
	}

	public String getName() {
		return name;
	}
}

Clients can perform the following operations on the Catalog:

  • check if an Item with a given identifier already exists,
  • add an Item to the Catalog,
  • remove an Item from the Catalog,
  • obtain an Item corresponding to a given identifier.

Client.java

public class Client {

	private final Catalog catalog;

	public Client(Catalog catalog) {
		this.catalog = catalog;
	}

	public void createItemAndAddToCatalog(String catalogPrefix, int catalogNumber) {
		Identifier id = new Identifier(catalogPrefix, catalogNumber);
		Item item = new Item(id, "item created by A");
		catalog.addItem(item);
	}

	public boolean checkIfAnItemForPrefixAndNumberExists(String catalogPrefix, int catalogNumber) {
		Identifier id = new Identifier(catalogPrefix, catalogNumber);
		return catalog.isPresent(id);
	}
	
	public void removeItemFromTheCatalogForPrefixAndNumber(String catalogPrefix, int catalogNumber) {
		Identifier id = new Identifier(catalogPrefix, catalogNumber);
		catalog.remove(id);
	}
	
	public Item obtainItemFromTheCatalog(String catalogPrefix, int catalogNumber) {
		Identifier id = new Identifier(catalogPrefix, catalogNumber);
		return catalog.getItem(id);
	}
}

Identifier is a combination of a String prefix and an integer. Here is the easiest implementation of this class.

Identifier.java

/**
 * First implementation of Identifier -- without hashCode() and equals().
 */
public class Identifier {

	private final String catalogPrefix;
	private final int catalogNumber;

	public Identifier(String catalogPrefix, int catalogNumber) {
		this.catalogPrefix = catalogPrefix;
		this.catalogNumber = catalogNumber;
	}

	public String getCatalogPrefix() {
		return catalogPrefix;
	}

	public long getCatalogNumber() {
		return catalogNumber;
	}
}

Now, let’s consider the following scenario: Client A creates an Item with an identifier [“cat-1”, 0] and adds it to the Catalog (“cat-1” serves as a catalog prefix and 0 is a number of an item). After some time Client B would like to find out if an item with an identifier consisting of the same prefix and number is present in the Catalog. The following snippet describes this situation.

AddAndCheckScenario.java

/**
 * In this scenario Client A adds an Item to the Catalog. Client B tries to
 * check whether an Item for an Identifier consisting of the same prefix and
 * number is already in the Catalog.
 */
public class AddAndCheckScenario {

	public static void main(String[] args) {
		Catalog catalog = new Catalog();
		Client clientA = new Client(catalog);
		Client clientB = new Client(catalog);

		clientA.createItemAndAddToCatalog("cat-1", 0);

		boolean isPresent = clientB.checkIfAnItemForPrefixAndNumberExists("cat-1", 0);

		System.out.println("Is an Item present for Client B? " + isPresent);
	}
}

If you run it, you should get the following result:

 
Is an Item present for Client B? false

Why is that so? Client A and Client B are constructing Identifier instances which consists of the same prefix and number – yet they are different objects. And because we did not override equals() for the Identifier class, they are not considered equal. An Item added by Client A is not accessible: other Clients cannot obtain this Item or remove it – that is a potential memory leak!

Let’s change the Identifier implementation by adding the equals() method.

Identifier.java

public class Identifier {
	
	private final String catalogPrefix;
	private final int catalogNumber;

	public Identifier(String catalogPrefix, int catalogNumber) {
		this.catalogPrefix = catalogPrefix;
		this.catalogNumber = catalogNumber;
	}

	public String getCatalogPrefix() {
		return catalogPrefix;
	}

	public long getCatalogNumber() {
		return catalogNumber;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Identifier)) {
			return false;
		}
		Identifier id = (Identifier) obj;
		return catalogPrefix.equals(id.getCatalogPrefix()) && catalogNumber == id.getCatalogNumber();
	}
}

Let’s run the AddAndCheckScenario again. We can see that adding equals() was not sufficient – an object created by Client A still cannot be found by Client B. What is the problem? Well, the answer is simple: the hashCode() contract is not obeyed. We have provided a custom equals() implementation without overriding hashCode() properly. Even if two Identifier instances having the same prefix and number are considered equivalent, they most probably have different hash codes. When looking for an Identifier instance created by Client B in the hashed collection inside the Catalog, different bucket is referenced – and that is why the object is not found.

To fix this we have to provide hashCode() implementation correspondent to previously introduced equals() method.

Identifier.java

public class Identifier {
	
	private final String catalogPrefix;
	private final int catalogNumber;

	public Identifier(String catalogPrefix, int catalogNumber) {
		this.catalogPrefix = catalogPrefix;
		this.catalogNumber = catalogNumber;
	}

	public String getCatalogPrefix() {
		return catalogPrefix;
	}

	public long getCatalogNumber() {
		return catalogNumber;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof Identifier)) {
			return false;
		}
		Identifier id = (Identifier) obj;
		return catalogPrefix.equals(id.getCatalogPrefix()) && catalogNumber == id.getCatalogNumber();
	}
	
	@Override
	public int hashCode() {
		int hashCode = 47;
		hashCode = hashCode * 17 + catalogPrefix.hashCode();
		hashCode = hashCode * 17 + catalogNumber;
		return hashCode;
	}
}

Now we see that Catalog’s answer is as expected:

 
Is an Item present for Client B? true

Conclusion

Properly implementing equals() and hashCode() is necessary when you expect your objects to be stored in the collection – especially a hashed one. To avoid memory leaks always remember to override hashCode() when you override equals()!

Download this sample code here.

This code is available on our GitHub repository as well.

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>