Entendiendo la relación entre ‘Equals()’ & ‘HashCode()’

Recientemente he empezado a leer Effective Java de Joshua Bloch, una lectura casi obligada en el mundo Java para empezar a escribir código eficiente. De entre los muchos temas que en el libro se tratan, hoy me ha parecido conveniente hablar de la estrecha relación que existe entre los métodos ‘equals()’ y ‘hashCode()’.

Tarde o temprano llega el momento en el que necesitas comparar tus objetos entre sí para determinar si son iguales o no, para ello, sobrescribir el método ‘equals()’ es el camino a seguir. Ocurre sin embargo, ya sea por desconocimiento o por pereza, que la mayoría de veces olvidamos implementar también el método ‘hashCode()’ contribuyendo así, y sin saberlo, a la proliferación de pequeños y molestos ‘bugs’ en nuestro código.

Veamos un ejemplo sencillo, una clase que representa el concepto ‘Persona’ con tres propiedades y un constructor:

public class Persona {

	String dni;
	String nombre;
	int edad;

	public Persona(String dni, String nombre, int edad) {
		super();
		this.dni = dni;
		this.nombre = nombre;
		this.edad = edad;
	}
}

Pongamos que decidimos que dos objetos ‘Persona’ son iguales si sus tres propiedades son iguales, con esto en mente nos servimos de la funcionalidad de nuestro IDE (Ecplise en mi caso) para generar automáticamente el método ‘equals()’ para esas propiedades, algo que quedaría similar a lo siguiente:

@Override
public boolean equals(Object obj) {

	if (this == obj) {
		return true;
	}

	if (obj == null) {
		return false;
	}

	if (!(obj instanceof Persona)) {
		return false;
	}

	Persona other = (Persona) obj;

	if (dni == null) {
		if (other.dni != null)
			return false;
	}
	else if (!dni.equals(other.dni)) {
		return false;
	}

	if (edad != other.edad) {
		return false;
	}

	if (nombre == null) {
		if (other.nombre != null)
			return false;
	}
	else if (!nombre.equals(other.nombre)) {
		return false;
	}

	return true;
}

En este punto tenemos una implementación propia para el ‘equals()’ y la implementación por defecto de ‘hashCode()’ que nos ofrece la clase Object. Dicho lo cual, vamos a ejecutar una pequeña prueba y analizaremos el resultado:

El test:

public class MainTest {
	public static void main(String[] args) {
		// Creamos dos objetos persona distintos con los mismos datos
		Persona persona1 = new Persona("00000014Z", "Oscar", 26);
		Persona persona2 = new Persona("00000014Z", "Oscar", 26);

		// Mostramos sus HashCode
		System.out.println("HashCode persona1->'" + persona1.hashCode() + "'");
		System.out.println("HashCode persona2->'" + persona2.hashCode() + "'");

		// Los comparamos
		System.out.println("¿Son iguales? resultado->'" + persona1.equals(persona2) + "'");

		// Creamos un pequeño HashMap para almacenar personas
		Map<Persona, String> map = new HashMap<Persona, String>();

		// Añadimos persona1 al HashMap
		map.put(persona1, "¡Visita RizandoElRizo.com!");

		// Comprobamos si el HashMap contiene a persona2, si los dos objetos son
		// iguales así debería ser.
		System.out.println("¿El HashMap contiene a persona2? resultado->'" + map.containsKey(persona2) + "'");
	}
}

El resultado:

HashCode persona1->'11394033'
HashCode persona2->'4384790'
¿Son iguales? resultado->'true'
¿El HashMap contiene a persona2? resultado->'false'

¿Qué está pasando aquí? persona1 y persona2 tienen ‘hashcodes’ distintos, parece ser que son ‘iguales’ pero no podemos usar uno para encontrar al otro dentro de un HashMap aún teniendo los mismos datos. El problema es sencillo, al sobrescribir nuestro ‘equals()’ hemos ignorado una de las clausulas del contrato del método ‘hashCode()’, si consultamos su javadoc veremos algo del estilo:

“If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.”

No hay duda, si queremos cumplir esta clausula siempre que implementemos uno deberíamos implementar el otro también. Hay que tener en cuenta que el método Object.hashCode() devuelve un entero relacionado con la dirección de memoria del objeto en cuestión, por lo tanto, no nos sirve. La siguiente pregunta es bastante obvia:  ¿Cómo creamos un ‘hashCode()’ de forma adecuada? Seamos prácticos y dejemos que nuestro IDE haga el trabajo por nosotros:

	@Override
	public int hashCode() {

		// Escogemos un número primo para los cálculos, tradicionalmente el 31
		final int prime = 31;

		// Inicializamos el resultado con un valor distinto de 0 para minimizar
		// las colisiones
		int result = 17;

		// Para cada una de las propiedades a tratar:
		// - Calculamos su hashCode en función de su tipo.
		// - Aplicamos la formula: 'resultado = numPrimo * resultado + hashCodePropiedad'.
		result = prime * result + ((dni == null) ? 0 : dni.hashCode());
		result = prime * result + edad;
		result = prime * result + ((nombre == null) ? 0 : nombre.hashCode());

		return result;
	}

Eclipse lo ha generado siguiendo las directrices que aparecen en Effective Java (Item 9), ciertamente no es casualidad. Si ahora volvemos a ejecutar el test obtenemos el resultado esperado:

HashCode persona1->'1143596956'
 HashCode persona2->'1143596956'
 ¿Son iguales? resultado->'true'
 ¿El HashMap contiene a persona2? resultado->'true'

Generar un ‘hashcode’ de forma eficiente no es algo trivial, el objetivo aquí es tener una idea clara de como funciona el asunto y salir del paso con una solución decente para la mayoría de casos.

En conclusión, al final uno siempre tiene que tener en cuenta lo siguiente cuanto trate con estos métodos:

  • Siempre que implementes ‘equals()’ debes implementar ‘hashCode()’.
  • Ambos métodos deben tratar los mismos atributos, de lo contrario perderás fácilmente la consistencia entre ambos.
  • Si dos objetos son iguales, su’ hashcode’ también lo debe ser.
  • Hay que tener en cuenta que dos objetos pueden tener el mismo ‘hashcode’ y ser diferentes a la vez, distintas combinaciones de propiedades podrían originar el mismo valor, el cálculo no garantiza un valor único.
  • La mayoría de Colecciones de Java basadas en ‘Hashes’ utilizan el método ‘hashCode()’ para realizar comparaciones de forma eficiente, si no lo implementas bien, tendrás problemas como el visto aquí.
  • Consulta la documentación de ambos métodos para tener claro sus características: Object.equals() y Object.hashCode()

Pero todavía podemos mejorar esto, ¿Qué ocurre si en nuestras clases tenemos un número importante de propiedades? Sencillo, el método ‘equals()’ se vuelve una monstruosidad, con tres propiedades hemos generado un método con 8 o 9 sentencias ‘if’, si tenemos 15 propiedades, haz números. Para solventar esto, la fundación Apache acude al rescate con la librería Apache Commons y las clases HashCodeBuilder y EqualsBuilder, tema central de mi próxima entrada. 🙂