Cómo sobreescribir los métodos equals y hashCode de Java

6 julio, 2010 por Ana Buigues Dejar una respuesta »

En la clase java.lang.Object (y por lo tanto, por herencia, en todas las demás clases) tenemos métodos que a veces olvidamos y que son importantes:

  • public boolean equals(Object o)
  • public int hashCode()

Estos métodos son especialmente importantes si vamos a guardar nuestros objetos en cualquier tipo de colección: listas, mapas… y más aun si los objetos que vamos a guardar en la colección son serializables.

Estos métodos tienen formas explicitas de cómo hay que implementarlos. Sobreescribir estos métodos puede parecer simple, pero en realidad hay muchas formas de hacerlo incorrectamente lo que nos puede llevar a muchos crebraderos de cabeza. Lo vemos a continuación.

Sobreescribir el método equals

Cuando sobreescribimos el método equals tenemos que tener en cuenta lo que se especifica en el API de Java para Object sobre este método. Debe cumplir las siguientes propiedades:

  • Reflexiva: para cualquier referencia no nula para un valor x, x.equals(x) debe devolver true.
  • Simétrica: para cualquier referencia no nula para valores x e y, x.equals(y) debe devolver true sii y.equals(x) devuelve true.
  • Transitiva: para cualquier referencia no nula para valores x, y,z, si x.equals(y) devuelve true y y.equals(z) devuelve true, entonces x.equals(z) debe devolver true.
  • Consistente: para cualquier referencia no nula para valores x e y, múltiples llamadas al método x.equals(y) deben consistentemente devolver siempre true o consistentemente devolver false. Siempre y cuando no se modifique la información usada en las comparaciones.
  • No nulo: para cualquier referencia no nula para un valor x, x.equals(null) debe devolver false.

Ahora que sabemos lo que tiene que cumplir, vamos a ver que pasos podemos seguir para su implementación:

  • Usamos el operador == para comprobar si el argumento es una referencia al mismo objeto.
  • Usamos el operador instanceof para comprobar si el argumento es un objeto de nuestra clase.
  • Hacemos un cast del argumento al nuestro objeto. Ya sabemos que es una instancia de nuestro objeto, por el paso anterior.
  • Para cada campo significativo de nuestro objeto, comprobamos que se corresponda con el que se pasa como argumento. Primero comprobamos los tipos primitivos y luego los más complejos. Para los tipos Float y Double, usamos los métodos Float.Compare y Double.Compare. Para el tipo String, usamos el equals del string. Para comparar arrays usamos Arrays.equals. Tenemos que tener en cuenta que los campos pueden contener referencias a null. No debemos incluir campos que tengan que ver con el estado de un objeto, como por ejemplo tipos Lock. Tampoco tenemos que incluir campos que sean cálculos de otros campos, es redundante.
  • Cuando terminemos, tenemos que preguntarnos: ¿es reflexivo?, ¿es simétrico?, ¿es transitivo?, ¿es consistente? y ¿no nullo?
//Ejemplo de cómo sobreescribir el método equals()
public class Casa {
 private int num;
 private String direccion;
 private double precio;
 private List<Propietario> prop;
 public Casa(int num, String direccion, double precio,
            List<Propietario> prop) {
  this.num = num;
  this.direccion = direccion;
  this.precio = precio;
  this.prop = prop;
 }
 @Override
 public boolean equals(Object o) {
  if (o == null)
   return false;
  if (o == this)
   return true;
  if (!(o instanceof Casa))
   return false;
  Casa c = (Casa) o;
  if (num != c.num)
   return false;
  if (direccion == null || !direccion.equals(c.direccion))
   return false;
  if (Double.compare(precio, c.precio) != 0)
   return false;
  if (prop != c.prop && (prop == null || !prop.equals(c.prop)))
   return false;
 return true;
 }
}
public class Propietario {
 private String nombre;
 private String apellidos;
 private int num;
 public Propietario(String nombre, String apellidos, int num) {
  this.nombre = nombre;
  this.apellidos = apellidos;
  this.num = num;
 }
 @Override
 public boolean equals(Object o) {
  if (o == null)
   return false;
  if (o == this)
   return true;
  if (!(o instanceof Propietario))
   return false;
  Propietario p = (Propietario) o;
  if ((nombre == null) ? (p.nombre != null) : !nombre.equals(p.nombre))
    return false;
  if ((apellidos == null) ? (p.apellidos != null) :
     !apellidos.equals(p.apellidos))
    return false;
  if (num != p.num)
    return false;
  return true;
 }
}

Sobreescribir el método hashCode

Siempre que sobreescribamos el método equals, también tenemos que sobreescribir también el método hashCode. En el API de java para Object del método hashCode se especifica lo siguiente:

  • Cuando este método es invocado sobre el mismo objeto una o más veces durante una ejecución en una aplicación, el hashCode debe de ser consistente devolviendo siempre el mismo valor, siempre que no se modifique el objeto. Este valor no tiene que ser consistente entre ejecuciones distintas de la aplicación.
  • Si dos objetos son iguales segun el método equals, entonces el hashCode de los dos objetos tiene que ser el mismo.
  • Si dos objetos no son iguales, el hashCode no tiene que ser necesariamente distinto, pero es recomendable que lo sea.

Unos pasos para implementar un buen método hashCode son:

  • Declaramos una variable entera y le asignamos un número, por ejemplo result=17.
  • Para cada campo significativo de nuestro objeto, f:
    • Calculamos en int c el valor de:
      • Tipo boolean: hacemos (f?1 : 0)
      • Tipos byte, char, short, o int: hacemos (int) f
      • Tipo Long: hacemos (int)(f^(f>>>32))
      • Tipo Float: hacemos Float.doubleToIntBits(f)
      • Tipo Double: hacemos Double.doubleToLongBits(f) (int)(f^(f>>>32))
      • Si es una referencia a un objeto, llamamos al hashCode del objeto. Si la referencia es nula, devolvemos un 0.
      • Si es un Array, utilizamos el método Arrays.hashCode.
    • Acumulamos el valor de c en result : result = 31 * result + c
  • Devolvemos el valor de result

Podemos excluir los campos que no comprobemos en el método equals, pero no es recomendable.

//Ejemplo de cómo sobreescribir el método hashCode
@Override
public int hashCode() {
  int result = 17;
  result = 31 * result + num;
  result = 31 * result + (direccion != null ?
           direccion.hashCode() : 0);
  result = 31 * result + (int) (Double.doubleToLongBits(precio)
           ^((Double.doubleToLongBits(precio) >>> 32));
  result = 31 * result + (propietarios != null) ?
           propietarios.hashCode() : 0);
 return result;
 }

Fuente: Libro Effective Java de Joshua Bloch

14 comentarios

  1. Sebastian Cardona dice:

    Hola Ana,

    Felicitaciones!!!… me encanta la combinación de programación, ingles y diseño…. XD.. Excelente trabajo y espero que continúes con tu blog…

    Lo primero es agradecer, me fue realmente útil llevo un año con Java y nunca había visto la importancia de estos de sobrescribir esos métodos. Hallado que también es importante para el unitTesting; por que cuando se usa assertEquals(); en realidad se evaluada con el equals() del objeto, o eso parece según mis apreciaciones(Me devolvía False hasta que lo sobrescribí). Quisiera preguntar algo; Es necesario utilizar, cito:
    if (direccion == null || !direccion.equals(c.direccion))
    Sabiendo que en la docs del String.equals() indica que devuelve true, si no es null y es la misma cadana de caracteres. Creo que solo sería necesario esto:
    if (direccion.equals(c.direccion))
    Es para ir a favor de no repetir código… Mira y este código lo genero el Netbeans para el hashCode(), no sé si esta correcto:

    int hash = 3;
    hash =
    79 * hash + (this.nombre != null ? this.nombre.
    hashCode() : 0);
    hash =
    79 * hash + (this.simbolo != null ? this.simbolo.
    hashCode() : 0);
    hash =
    79 * hash + Float.floatToIntBits(
    this.valorConversion);
    return hash;

    De nuevo muchas gracias por compartir… Que viva la cultura geek y la cultura Libre…
    Salu2 desde Colombia…

  2. Ana Buigues dice:

    Hola Sebastian,
    Efectivamente sí es necesaría la comprobación de null, es para evitar que al hacer:
    direccion.equals(c.direccion)
    el método equals devuelva la excepción NullPointerException.

    Puedes probar el siguiente código para verlo:
    String direccion = null
    direccion.equals("loquesea")
    Verás como produce un NullPointerException

    Si te fijas, el generador del hashCode del netbeans hace la misma comprobación: (suponiendo que el nombre es un String)
    (this.nombre != null ? this.nombre.hashCode() : 0)

    La verdad es que las últimas versiones del netbeans generan el equals y el hashcode siguiendo las propiedades, con lo que normalmente con ésta generación automática te será suficiente.

    Muchas gracias por tu comentario :)

  3. Sebastian Cardona dice:

    Hola Ana,
    Gracias por tu respuesta, efectivamente el Netbeans los genera… lo anoto a mi lista de descuidos…XD…

    Quisiera comentar algo ya que hablas del NullPointerException, en Python vengo programando poco tiempo y en su Zen indican que: “Los errores nunca deberían dejarse pasar silenciosamente.”; ahora eso es Python, pero si lo llevamos a Java?… No sé, pero muchas veces una variable null me la ha jugado; puede que esto se deba a mi inexperiencia o a mi modo de programar…No sería más fácil darnos cuenta de lo que pasa con la variable en ese momento?… Puede este envuelto en otro de tantos paradigmas de programación y por eso me parezca conveniente…XD…

    Salu2

  4. Ana Buigues dice:

    Hola Sebastian, en Java tenemos diferentes tipos de excepciones dependiendo del tipo de error que representen. Todas ellas descienden de la clase Throwable. Aquí
    http://en.wikibooks.org/wiki/File:Java_exception_classes.jpg puedes ver el diagrama de clases que muestra cómo están relacionadas las excepciones en Java.

    Throwable tiene dos descendientes directos:
    * Error: Se refiere a errores graves en la máquina virtual de Java, como por ejemplo fallos al enlazar con alguna librería. Normalmente en los programas Java no se tratarán este tipo de errores.
    * Exception: Representa errores que no son críticos y por lo tanto pueden ser tratados y continuar la ejecución de la aplicación. La mayoría de los programas Java utilizan estas excepciones para el tratamiento de los errores que puedan ocurrir durante la ejecución del código.

    Dentro de Exception, tenemos las RuntimeException de la cual derivan todas aquellas excepciones referidas a los errores que comúnmente se pueden producir dentro de cualquier fragmento de código, como por ejemplo hacer una referencia a un puntero null, o acceder fuera de los límites de un array.

    Estas RuntimeException se diferencian del resto de excepciones en que no son de tipo checked. Una excepción de tipo checked debe ser capturada o bien especificar que puede ser lanzada de forma obligatoria, y si no lo hacemos obtendremos un error de compilación. Dado que las RuntimeException pueden producirse en cualquier fragmento de código, sería impensable tener que añadir manejadores de excepciones y declarar que éstas pueden ser lanzadas en todo nuestro código. Deberemos:
    * Utilizar excepciones unchecked (no predecibles) para indicar errores graves en la lógica del programa, que normalmente no deberían ocurrir.
    * Utilizar excepciones checked para mostrar errores que pueden ocurrir durante la ejecución de la aplicación, normalmente debidos a factores externos como por ejemplo la lectura de un fichero con formato incorrecto, un fallo en la conexión, o la entrada de datos por parte del usuario.

  5. Marcos Jara dice:

    Hola Ana,

    Buenisimo artículo, de hecho estoy preparando un entrenamiento para mis alumnos de la Universidad y voy a utilizar parte de la información que publicaste aqui… si me lo permites!!! :=)

    Un cordial Saludo.

  6. Gabriel dice:

    Buen articulo y me ha sido de gran ayuda , gracias.

  7. Pablo dice:

    La verdad excelente articulo me ha sido de mucha ayuda.

    Muchas gracias!!

  8. Gaizka dice:

    Buenas

    Buena artículo. Quería hacerte un apunte.

    Dices: “Podemos excluir los campos que no comprobemos en el método equals, pero no es recomendable.”

    Se deben excluir porque sino no garantizarías “Si dos objetos son iguales segun el método equals, entonces el hashCode de los dos objetos tiene que ser el mismo”.

    Un saludo

  9. Efra dice:

    Ana, una duda, en todos lados veo que para el hashCode involucran siempre el número 31… ¿Es un tipo de convención? ¿Puedo usar mi propia manera de crear el hashCode? ¿Hay una implementación de referencia?

    Saludos.

  10. Jose dice:

    Hola Ana, ¿cómo se sobrescribiría el método equals para un tipo enumerado?

    Gracias.

  11. Jose dice:

    Hola, buena mañana, una consulta, deseo sobreescribir un método que tiene tres variables, dos son de tipo String y una de tipo Double. Cuando haga el @Override y declare el tipo de la variable que voy a sobreescribir ¿cómo sería su declaración? Envío el ejemplo, la variable precio es de tipo Double, el toString sé que va enviar un dato de tipo String y otro de tipo booleano pero ¿qué ocurre con la variable precio? ¿cómo sería su correcta declaración?

    @Override
    public String toString(){
    return “ProductoBean: ” + this.nombre +this.tipo+this.precio;
    }

Trackbacks /
Pingbacks

  1. Berry
  2. his explanation

Deja un comentario