EqualsBuilder & HashCodeBuilder, mejorando lo visto

Siguiendo lo comentado en la entrada anterior, cuando nuestras clases empiezan a acumular un buen número de propiedades, generar de forma automática el método ‘equals()’ engendra un monstruo.

Si bien podemos refactorizar e intentar maquillar el asunto, lo más probable es que caigamos en lo que se conoce como un antipatrón, y más concretamente acabaremos reinventando la rueda cuadrada. ¿Qué significa esto? Pues que gastaremos tiempo en hacer algo que ya se ha hecho con anterioridad y seguramente nuestra solución sea ‘peor’ que la que ya existe.

Esta situación ocurre a diario, cuando estamos aprendiendo nos viene bien para ejercitar un poco el coco pero cuando tenemos plazos de entrega, hay que ser práctico. En muchas ocasiones me sorprendo a mi mismo uniendo piezas ajenas para llevar a cabo pequeñas funcionalidades, esto no es malo mientras entiendas lo que estas haciendo, de lo contrario la cosa puede degenerar en una variante de la programación basada en el copiar/pegar (Otro antipatrón).

Pero no nos desviemos del tema, necesitamos encontrar una solución para nuestro problema con los ‘equals()’. Las preguntas clave que hay que hacerse son:

  1. ¿Soy el primero en enfrentarme a este problema? (Probablemente no)
  2. ¿Habrá alguna solución ya existente para este problema? (Probablemente sí)

Es aquí donde entra en juego la fundación Apache y sus archiconocidas librerías Apache Commons, sin entrar en grandes detalles, estas librerías te harán la vida más fácil como programador java, poco a poco iremos examinándolas con más detalle.

Imagen

Concretamente para este caso, las clases EqualsBuilder y HashCodeBuilder de la librería Apache Commons Lang cubren nuestras necesidades. Vamos a verlas en acción, comencemos por el método ‘equals()’ de la clase ‘Persona’ utilizada en la entrada anterior:


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

		EqualsBuilder eBuilder = new EqualsBuilder();

		eBuilder = eBuilder.append(this.dni, other.dni);
		eBuilder = eBuilder.append(this.edad, other.edad);
		eBuilder = eBuilder.append(this.nombre, other.nombre);

		return eBuilder.isEquals();
	}

La mejora en cuanto a legibilidad y mantenibilidad es muy notable, veamos si sucede igual con el método ‘hashCode()’:


	@Override
	public int hashCode() {

		// Creamos una instancia, el primer valor es para la
		// inicialización, el segundo el número primo para las
		// multiplicaciones
		HashCodeBuilder hcBuilder = new HashCodeBuilder(17, 31);

		hcBuilder.append(this.dni);
		hcBuilder.append(this.edad);
		hcBuilder.append(this.nombre);

		return hcBuilder.toHashCode();
	}

La respuesta es afirmativa, el código se simplifica a costa de añadir una libería (‘jar’) a nuestro proyecto. Además podemos estar tranquilos en lo que se refiere a su implementación, el método ‘hashCode()’ esta hecho siguiendo las directrices de Effective Java (Igual este libro es interesante :)).

No he comentado nada en relación a como añadir la librería, todo depende de como gestionemos las dependencias en nuestro proyecto, para este ejemplo no me he complicado en exceso y he añadido el ‘jar’ de la librería (‘commons-lang3-3.1.jar’) al Classpath del proyecto, con Maven añadiríamos la dependencia en el POM de nuestro proyecto (Un tema interesante para una futura entrada).

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