lunes, 3 de septiembre de 2012

Cómo implementar correctamente hashCode() e equals() en Java

Este post no intenta ser una discusión filosófica sobre la correcta forma de implementar equals y hashCode sino que sólo intenta mostrar la forma práctica de hacerlo con ejemplos.

Vamos primero con los ejemplos para aquellos que llegan acá buscando la solución sin leer mucho. Al final está la teoría.

Conozco dos muy buenas bibliotecas con utilidades para implementar equals y hashCode, así que doy un ejemplo con cada una de ellas.

Ejemplo usando jakarta commons-lang

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class MyClass {
 private String field1;
 private int field2;
 private double field3;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MyClass)) {
   return false;
  }
  MyClass other = (MyClass) obj;

  // Now we compare field by field
  return new EqualsBuilder()
      .append(this.field1, other.field1)
      .append(this.field2, other.field2)
      .append(this.field3, other.field3)
      .isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder()
      .append(field1)
      .append(field2)
      .append(field3)
      .toHashCode();
 }
}

class MySubClass extends MyClass {
 private String field4;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MySubClass)) {
   return false;
  }
  MySubClass other = (MySubClass) obj;

  // Now we compare field by field
  return new EqualsBuilder()
      .appendSuper(super.equals(obj)) // Note the call to super
      .append(this.field4, other.field4)
      .isEquals();
 }

 @Override
 public int hashCode() {
  return new HashCodeBuilder()
      .appendSuper(super.hashCode()) // Note the call to super
      .append(field4)
      .toHashCode();
 }
}

Ejemplo usando Guava

import com.google.common.base.Objects;

public class MyClass {
 private String field1;
 private int field2;
 private double field3;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MyClass)) {
   return false;
  }
  MyClass other = (MyClass) obj;

  // Now we compare field by field
  return Objects.equal(this.field1, other.field1)
      && Objects.equal(this.field2, other.field2)
      && Objects.equal(this.field3, other.field3);
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(field1, field2, field3);
 }
}

class MySubClass extends MyClass {
 private String field4;

 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (!(obj instanceof MySubClass)) {
   return false;
  }
  MySubClass other = (MySubClass) obj;

  // Now we compare field by field
  return super.equals(obj) // Note the call to super
      && Objects.equal(this.field4, other.field4);
 }

 @Override
 public int hashCode() {
  return Objects.hashCode(super.hashCode(), field4); // Note the call to super
 }
}

La teoría

La regla es que si se implementa equals(), también se debe implementar hashCode() porque siempre se debe cumplir lo siguiente:
  • Si dos objetos son iguales, deben tener el mismo hashCode.
  • Si dos objetos tienen diferente hashCode entonces deben ser distintos.

En el caso del equals, también es necesario que se cumpla lo siguiente:
Dados los objetos A, B y C:
  • Si A.equals(B) entonces B.equals(A) y viceversa
  • A.equals(A) debe ser true
  • A.equals(null) debe ser false
  • Si A.equals(B) y B.equals(C), entonces debe cumplirse que A.equals(C)
La única regla que no se cumple del todo con los ejemplos es la primera, en el caso del equals de las subclases.

 @Test
 public void testEqualsObject() {
  MyClass a = new MyClass();
  MyClass b = new MySubClass();
  assertTrue(a.equals(b)); // Returns true and perhaps it shouldn't
  assertFalse(b.equals(a)); // Returns false
 }
Una alternativa es implementar el comienzo del equals así:
  @Override
  public boolean equals(Object obj) {
    if (obj == null) {
      return false;
    }
    if (obj == this) {
      return true;
    }
    if (obj.getClass() != getClass()) {
      return false;
    }

    MyClass other = (MyClass) obj;

    // Now we compare field by field
    //...
  }

La desventaja es que las subclasses deberían implementar el equals teniendo en cuenta los campos de la superclase sin poder llamar a super.equals().

Como sea, es algo a tener en cuenta y cada uno deberá balancear las ventajas y desventajas de cada alternativa.

Apéndice: Si querés incluir estas bibliotecas en maven

<dependency>
    <groupid>org.apache.commons</groupid>
    <artifactid>commons-lang3</artifactid>
    <version>3.1</version>
</dependency>
<dependency>
    <groupid>com.google.guava</groupid>
    <artifactid>guava</artifactid>
    <version>13.0</version>
</dependency>




No hay comentarios.: