| Java Hashing |
|
|
|
| Escrito por Leonardo De Seta |
| Martes 27 de Octubre de 2009 10:22 |
|
En este artículo veremos porqué y cómo sobreescribir el método hashCode() que cumpla con el contrato para los HashCode. El contrato de un HashCodeEl contrato del hashCode() dice: "Si dos objetos son iguales usando equals(), entonces la invocación a hashCode() de ambos objetos debe retornar el mismo valor" Entonces, la pregunta que surge es: ¿es necesario que siempre se cumpla esa oración? Consideremos una clase que tiene una implementación correcta del método equals(), ¿qué pasaría si no obedecemos el contrato anterior? Para responder a esa pregunta, vamos a tener que considerar dos situaciones:
Objetos que son iguales, pero retornan diferentes hashCodes¿Qué pasaría si dos objetos son iguales (invocando equals()) pero retornan diferentes hashCodes? El código se ejecutará a la perfección. Nunca vamos a encontrar problemas... hasta que se nos ocurra almacenar a nuestro objeto dentro de una colección como un HashSet o un HashMap. Cuando hagamos esto, nos vamos a encontrar con problemas raros durante la ejecución. Primero tenemos que comprender cómo funcionan las clases del tipo HashSet y HashMap. Estas clases de colecciones dependen de que los objetos que son agregados cumplan con el contrato del hashCode. Vamos a obtener resultados impredecibles en tiempo de ejecución si no obedecemos el contrato y queremos almacenar estos objetos en la colección. Veamos por ejemplo el HashMap. Cuando guardamos valores en un HashMap, estos valores en realidad se almacenan dentro de "baldes". Cada uno de estos baldes tiene asignado un número que lo identifica. Cuando agregamos un valor al HashMap, almacena el dato en uno de esos baldes. El balde que se usa depende del hashCode que devuelva el objeto a ser almacenado. Por ejemplo, si el método hashCode() del objeto retorna 49, entonces se almacena en el balde 49 dentro del HashMap. Más tarde, cuando verifiquemos si la colección contiene al elemento invocando el método contains(elemento), el HashMap primero obtiene el hashCode de ese "elemento". Luego buscará el balde que corresponde a ese hashCode. Si el balde está vacio, significa que el HashMap no contiene al elemento y devuelve false. Si hay un objeto o más dentro del balde, entonces se compara al "elemento" con todos los elementos en ese balde usando el método equals(). Objetos que no son iguales, pero retornan el mismo hashCodeEl contrato del hashCode no dice nada sobre este caso. Por lo tanto, objetos distintos pueden devolver el mismo hashCode, pero las colecciones como los HashMap van a ser más ineficientes si se almacenan objetos diferentes con el mismo valor de hashCode. ¿Por qué almacenar en baldes?Se utiliza este mecanismo de "baldes" por un tema de eficiencia. Pueden imaginarse que si todos los objetos que se agregan a un HashMap se almacenaran en una única lista grande, entonces tendríamos que comparar la entrada con todos los objetos de la lista para dterminar si un elemento en particular está contenido en el Map. Como se usan baldes, sólo se comparan los elementos del balde específico, y en general cada balde sólo almacena una pequeña cantidad de elementos en el HashMap. Sobreescribir el método hashCode()Puede resultar complejo escribir un buen método de hashCode() para una clase nueva. Retornar un valor fijo (es una mala idea...)Podemos implementar un método de hashCode() que devuelva un valor fijo, como por ejemplo: //no hagan esto, genera mal rendimiento @Override public int hashCode() { return 1; } Este método satisface todos los requerimientos y es "legal" de acuerdo al contrato del hashCode, pero no va a resultar muy eficiente. Si se usa este método, todos los objetos se almacenarán dentro del mismo balde (el correspondiente al "1"), y cuando querramos comprobar si un objeto específico está dentro de la colección, entonces siempre se tendrá que verificar el contenido completa de dicha colección. Por otro lado, si sobreescribimos el método hashCode() y rompemos el contrato ("dos objetos iguales con equals deben devolver el mismo hashCode"), entonces cuando se invoque el método contains() podría devolver false para un elemento que se encuentra dentro de la colección, pero en un balde diferente. Método de Effective JavaJoshua Bloch en su libro Effective Java nos brinda una buena guía para generar un valor de hashCode():
Veamos un ejemplo de este algoritmo: public class HashTest { private String campo1; private short campo2; //resto de la clase... @Override public int hashCode() { int result = 17; result = 37*result + campo1.hashCode(); result = 37*result + (int)campo2; return result; } } Como vemos elegimos la constante 37. La idea es ejegir un número que sea un número primo. Podemos elegir cualquier número primo. Al usar un número primo los objetos se distribuirán mejor en los baldes. Pueden aprender más sobre este algoritmo y la distribución que genera buscando en Internet. Apache HashCodeBuilderComo estamos aprendiendo, no es siempre facil retornar un buen valor de hashCode. Por suerte existen clases que nos pueden ayudar. El paquete org.apache.commons.lang.builder de Jakarta-Commons contiene la clase HashCodeBuilder que está diseñada para ayudarnos a implementar el método hashCode(). Muchos desarrolladores luchan por escribir sus hashCode cuando existe esta clase que nos simplifica el proceso. Así es como quedaría la clase de prueba anterior usando la clase HashCodeBuilder: public class HashTest { private String campo1; private short campo2; //resto de la clase... @Override public int hashCode() { return new HashCodeBuilder(83, 7) .append(campo1) .append(campo2) .toHashCode(); } } Noten que los dos números del constructor del HashCodeBuilder son dos números impares distintos a cero - estos números ayuda a evitar la colisión de valores de hashCode en otros objetos. Si se necesita, se puede invocar al hashCode() de la superclase usando appendSuper(int). Resulta muy facil escribir el método hashCode() usando la clase Apache HashCodeBuilder. Objetos mutables como claveComo consejo general, deberíamos usar objetos inmutables como clave en una colección. El hashCode funciona mejor cuando se calcula con datos inmutables. Si usamos objetos mutables como clave y estos objetos cambian su estado de manera que el hashcode también cambia, entonces el objeto almacenado quedará ubicado en un balde incorrecto dentro de la colección. La cosa más importante a consdierar cuando se implementa el hashCode() es que, sin importar cuándo se invoca a este método, tiene que producir el mismo valor para un objeto en particular cada vez que se invoca. Si tenemos un escenario en donde el objeto produce un valor de hashCode() cuando se invoca al put() del HashMap y luego produce otro valor durante un get(), en ese caso no podremos recuperar este objeto. Por lo tanto, si nuestro hashCode() depende de datos mutables en el objeto, cambiar estos datos con seguridad producirán una nueva clave al generar un hashCode() diferente. Veamos el siguiente ejemplo: public class Empleado { private String nombre; private int edad; public Empleado() { } public Empleado(String nombre, int edad) { this.nombre = nombre; this.edad = edad; } public String getNombre() { return nombre; } public void setNombre(String nombre) { this.nombre= nombre; } public int getEdad() { return edad; } public void setEdad(int edad) { this.edad = edad; } @Override public boolean equals(Object obj) { if (obj instanceof Empleado) { Empleado emp = (Empleado)obj; return (emp.nombre.equals(nombre) && emp.edad == edad); } return false; } @Override public int hashCode() { return nombre.length() + edad; } public static void main(String[] args) { Empleado e = new Empleado("muhammad", 24); Map map = new HashMap(); map.put(e, "Muhammad Ali Khojaye"); // encuentra el resultado System.out.println(map.get(e)); e.nombre = "abid"; // el map devolverá null porque no lo encuentra System.out.println(map.get(e)); // otra vez devolverá null System.out.println(map.get(new Empleado("muhammad", 24))); } } Vemos en el ejemplo anterior que obtenemos algunos resultados extraños. Después de cambiar el campo nombre, el cálculo del hashCode() devuelve un nuevo número y apuntará a un nuevo balde, por lo que el contains() devolverá false. Podemos arreglar esta situación usando alguna de estas alternativas:
Traducido de Java Hashing, publicado en DZone. |