17 de septiembre de 2011

Genericos y Colecciones Parte 1. Sobreescribiendo equals y hashCode

Genéricos y colecciones

Los genéricos fueron una de las características principales en el lanzamiento de Java 5. Con la salida de los genéricos cambio la forma en como podemos manejar colecciones en Java. Una de las características que tienen las colecciones es el poder ordenar y buscar entre los elementos que forman parte del contenido. Podemos establecer el criterio de búsqueda u ordenamiento para nuestras colecciones, pero para esto debemos sobrescribir los métodos equals y hashCode. En esta primera publicación tocaré el tema de sobrescribir los métodos equals y hashCode, las características de cada uno de estos métodos y algunos ejemplos.

Puntos a tocar en esta publicación:

  • Sobrescribiendo el método equals
  • Reglas que sigue el método equals
  • Ojo, mucho ojo.
  • Sobrescribiendo el método hashCode
  • ¿Que implica el hashcode?
  • Reglas que sigue el método hashCode
  • Ojo, mucho ojo.


Sobrescribiendo el método equals

Indagando en la javadoc acerca del método equals(Object o), podemos encontrar lo siguiente:

Método: equals(Object o)
Valor de retorno : boolean
Descripción: Indica si un objeto es igual a este.

Este método lo utilizamos para saber si un objeto es igual que otro. Este método utiliza el operador == para comparar a dos objetos y decidir si son iguales, por ejemplo podemos tener la siguiente clase:


public class Demo {

private int boleta;

public Demo(int boleta){
this.boleta = boleta;
}

public int getBoleta(){
return this.boleta;
}

public static void main(String[] args) {
Demo demoA = new Demo(20);
Demo demoB = new Demo(20);

System.out.println(demoA.equals(demoB));
}
}

La ejecución del anterior método main da como resultado en consola false. Esto es porque el método equals sin sobrescribir ocupa el operador == para comparar a dos objetos. Pero supongamos que queremos diferenciar a los objetos mediante su atributo boleta, ya que es el identificador de un Alumno para una aplicación escolar. Para esto debemos sobrescribir el método equals, diciéndole qué propiedad del objeto debe ser comparada para determinar si un objeto es igual a otro. El método sobrescrito se vería de esta forma:

@Override
public boolean equals(Object o){
if(( o instanceof Demo) && (((Demo)o).getBoleta() == this.boleta))
{
return true;
}else{
return false;
}
}

la tercera linea es la que tienen la magia. En ella hacemos un par de validaciones, la primera tiene que ver con estar seguros de que el objeto o es una instancia de la clase Demo, y la segunda es verificar si la propiedad boleta de el objeto o es igual a la propiedad boleta del objeto que invoco el método equals. Si los valores de boleta son los mismos, el método equals que sobrescribimos regresara true. Con esto nosotros tomamos la decisión sobre que criterio se debe tomar para comparar a nuestros objetos.

public static void main(String[] args) {
Demo demoA = new Demo(20);
Demo demoB = new Demo(20);

System.out.println(demoA.equals(demoB));
}

Por lo anterior, esta ejecución del método main, nos dará como resultado un true en la salida por consola.

Hay tipos de colecciones como los Sets que no nos permiten agregar objetos duplicados a la colección. La forma en como los Sets van a decidir si un objeto esta duplicado o no, la especificamos nosotros cuando sobrescribimos el método equals. Lo mismo cuando en una colección de tipo Hash* queremos buscar un elemento en especifico, la colección sabe que objeto regresar, dado el criterio que nosotros definimos al sobrescribir el método equals y hashCode. En esto radica la importancia de sobrescribir estos métodos, siempre y cuando este dentro de nuestras intenciones contar con este tipo de prestaciones.

Reglas que sigue el método equals

  • Reflexivo: Para cualquier referencia al valor x, x.equals(x) debe regresar true.
  • Simetrico: Para cualquier referencia a los valores x y z, x.equals(z) debe regresar true si y solo si z.equals(x) es true.
  • Transitivo. Para cualquier referencia a los valores w, x y z, si w.equals(x) regresa true y x.equals(z) regresa true, entonces w.equals(z) debe regresar true.
  • Consistente: Para cualquier referencia a los valores x y z, múltiples invocaciones a x.equals(z) consistentemente regresaran true o false, si es que los valores utilizados para la comparación de los objetos no ha sido modificada.
  • Para cualquier referencia no nula al valor x, x.equals(null), debe regresar false.

Ojo, mucho ojo

  • Asegúrate de sobrescribir el método equals. Las siguientes son implementaciones del método equals que son validas para el compilador, pero no validas para sobrescribir el método:
    • boolean equals(Objeto o). Esta implementación no sobreescribe el método equals de la clase Object, ya que el método debe ser declarado como public.
    • public boolean equals(Demo o). Esta implementación no sobreescribe el método equals de la clase Object, ya que el parámetro que necesita el método equals debe ser explícitamente un objeto de la clase Object, y no uno que extienda de éste. Esta implementación, al igual que la anterior es una sobrecarga del método equals, mas no sobreescribe este método.

La descripción correcta del método equals, es decir, la forma en como debe sobrescribirse es la siguiente:

public boolean equals(Objeto o);



Sobrescribiendo el método hashCode

¿Qué implica el hashcode?

Algunas colecciones usan el valor hashcode para ordenar y localizar a los objetos que están contenidos dentro de ellas. El hashcode es un numero entero, sin signo, que sirve en colecciones de tipo Hash* para un mejor funcionamiento en cuanto a performance. Este método debe ser sobrescrito en todas las clases que sobrescriban el método equals, si no se quiere tener un comportamiento extraño al utilizar las colecciones de tipo Hash* y otras clases. Si dos objetos son iguales según el método equals sobrescrito, estos deberian regresar el mismo hashcode. Véase el siguiente ejemplo:


public class Demo {

private int boleta;

public Demo(int boleta){
this.boleta = boleta;
}

public int getBoleta(){
return this.boleta;
}

@Override
public boolean equals(Object o){
if((o instanceof Demo) && (((Demo)o).getBoleta()== this.boleta))
{
return true;
}else{
return false;
}
}

public static void main(String[] args) {
Demo demoA = new Demo(20);
Demo demoB = new Demo(20);

System.out.println(demoA.equals(demoB));
System.out.println(demoA.hashCode());
System.out.println(demoB.hashCode());
}
}

Esto nos da como resultado:

true
1414159026
1569228633

regresa true porque estos objetos son iguales, debido a que se sobrescribió el método equals. Si son considerados iguales por equals, esto se reflejará al utilizarse en las colecciones de tipo Hash*. Lo que podemos ver en la salida de este código es que a pesar de que ambos objetos son iguales, el hashcode no es igual. Esto es porque no hemos sobrescrito el método hashCode.

El uso principal del hashcode, es como lo mencionamos arriba, cuando se manejan colecciones de tipo Hash*. La forma en como operan las colecciones de este tipo es a grandes rasgos la siguiente: Las colecciones de tipo Hash* almacenan los objetos en lugares llamados baldes, de acuerdo al numero obtenido por el método hashCode. Si el método hashCode regresa un 150, el objeto será guardado en el balde numero 150. Puede llegar a pasar que haya mas de un objeto de diferente tipo en el mismo balde. Esto no ocasiona ningún problema al momento de recuperar el objeto del balde, ya que al buscarlo este tipo de colecciones necesita como parámetro un objeto con el mismo valor hashcode, el cual utilizara para buscar el numero de balde que contiene a el objeto en cuestión. Si hay mas de un objeto, el siguiente criterio para determinar cual es el objeto buscado, es la utilización del método equals. Así es como este tipo de colecciones para buscar un objeto, ya sea para regresarlo y para ordenarlo. Lo cual falla si no sobrescribimos el método hashCode.

Sobrescribiendo el método hashCode

A continuación veremos como podremos sobrescribirlo:


@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + this.boleta;
return hash;
}

La salida que obtenemos una vez que se rescribe este método en el ejemplo de arriba es :

true
699
699

lo que nos dice que estos métodos son iguales según equals, y que además tienen el mismo numero hashcode.

Este método hashCode regresara consistentemente el mismo valor, siempre y cuando el campo boleta no cambie. Cada desarrollador puede implementar de diferente manera igualmente validas, correctas y eficientes este método. Lo que debemos de tener en mente al sobrescribir este método es que si para sobrescribir el método utilizamos variables de instancia (en nuestro ejemplo boleta), también debemos utilizar variables de instancia para generar un hashcode correcto. En el caso de las constantes que se utilizan en el ejemplo de arriba, se recomienda utilizar números primos, para una mejor distribución del hashcode generado.


Reglas que sigue el método hashCode

  • Si el método hashCode es invocado en múltiples ocasiones durante la ejecución de una aplicación, debe regresar consistentemente el mismo valor entero, esto si la información utilizada para calcular el hashcode no ha cambiado entre invocación e invocación del método hashCode.
  • Si dos objetos son iguales según el método equals, entonces la llamada al método hashCode debe regresar el mismo hashcode.
  • No es requerido que si dos métodos no son iguales según el método equals, tengan diferentes valores hashcode.

Ojo, mucho ojo

  • Si dos métodos son iguales según el método equals, el método hashCode debe regresar el mismo entero para ambos métodos. Sin embargo, si el métodos equals dice que dos métodos no son iguales, el método hashCode puede o no regresar el mismo entero.
  • Retornar un valor fijo en un método hashCode es una mala idea, ya que tendremos múltiples objetos con el mismo valor hashcode, lo cual no ayuda en nada a la hora de trabajar con colecciones de tipo Hash*.
  • Si utilizamos variables de clase de tipo transient para generar un hashcode, serializamos el objeto en cuestión y queremos recuperar el hashcode de ese método, nos encontraremos con que el hashcode será diferente al hashcode con que se serializó el objeto. Ya que este tipo de variables no se serializa. Por eso es una mala idea utilizar variables transient para generar el hashcode.