Como sabemos java es un lenguaje orientado a objetos que ha sido diseñado cuidadosamente. Cada objeto tanto de la librería estándar como las clases que creamos tienen por defecto definidos los métodos de la clase Object.

En este post estudiaremos como escribir nuestros métodos equals() y hascode(), cuando deberíamos escribirlos y cuando no.

equals()

Supongamos que tenemos la clase “Dinero” creada de la siguiente manera

class Dinero {
    int monto;
    String divisa;
    
    public Dinero(int monto, String divisa) {
      this.monto = monto;
      this.divisa = divisa;
    }
}

Y supongamos que queremos hacer la siguiente operación con esta clase:

Dinero ingresos = new Dinero(100, 'GTQ');
Dinero gastos = new Dinero(100, 'GTQ');
boolean balance = ingresos.equals(gastos);

Esperaríamos que como los dos objetos tienen un monto de 100 en la misma divisa, balance entonces sea true. Sin embargo la clase Dinero en su forma actual no se comporta de esa manera, ya que por defecto la el método equals() de la clase Object verifica que ambos sean el mismo objeto en lugar de verificar sus campos, y como ingresos no es el mismo objeto que gastos el resultado es false.

Sobrescribiendo equals()

@Override
public boolean equals(Object o) {
    if (o == this) {
        return true;
    }

    if (!(o instanceof Money)) {
        return false;
    }

    Dinero otro = (Dinero)o;
    boolean divisaIgual = (this.divisa == null && otro.divisa == null)
      || (this.divisaIgual != null && this.divisaIgual.equals(otro.divisaIgual));
    return this.monto == otro.monto && divisaIgual;
}

En el segmento de código anterior, vemos que seguimos soportando el comportamiento por defecto de revisar si son el mismo objeto, y que sean una instancia de la misma clase. Pero adicionalmente verificamos que los campos que nos interesan sean iguales para determinar si son iguales o no sin importar su referencia en memoria.

El contrato equals()

Según la documentación oficial el método equals debe cumplir el siguiente contrato para que podamos decir que esta bien implementado:

  • Reflexivo para cualquier referencia no nula de un valor x, x.equals(x) debe regresar true
  • Simétrico para cualquier referencia no nula de un valor x y un valor y x.equals(y) debe regresar lo mismo que y.equals(x)
  • Transitivo para un valor no nulo de x, y & z se debe cumplir: x.equals(y) y y.equals(z) entonces x.equals(z) son todos verdaderos.
  • Consistente Se puede invocar equals() tantas veces consecutivas como se quiera y deberá resolver siempre el mismo valor a menos que una de sus partes cambie.
  • Para cualquier valor no nulo de x, x.equals(null) debe resolver a false.

Violando la simetría de equals()

A simple vista el contrato equals() parece de sentido común (dah!). Pero con las malas prácticas correctas la clausula de simetría podría verse vulnerada. Supongamos que tenemos una clase Comprobante que hereda de nuestra clase Dinero

class Comprobante extends Dinero {
 
    private String tienda;
 
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Comprobante))
            return false;
        Comprobante otro = (Comprobante)o;
        boolean mismaDivisa = (this.divisa == null && otro.divisa == null)
          || (this.divisa != null && this.divisa.equals(otro.divisa));
        boolean mismaTienda = (this.tienda == null && otro.tienda == null)
          || (this.tienda != null && this.tienda.equals(otro.tienda));
        return this.monto == otro.monto && mismaDivisa && mismaTienda;
    }
}

Teniendo lo anterior hacemos estas operaciones:

Dinero pago = new Dinero(42, "GTQ");
Comprobante comprobante = new Comprobante(42, "GTQ", "Amazon");
 
comprobante.equals(pago) => false // como esperabamos U_U.
pago.equals(comprobante) => true // whaaat?.

Claramente la herencia vulnero el principio de simetría de nuestro método equals().

Arreglando equals() con composición

Para evitar caer en esta trampa del destino, debemos en lugar de usar herencia crear un campo en la clase Comprobante que sea del tipo Dinero y usar el equals() propio de la clase Dinero dentro del equals propio de la clase Comprobante.

class Comprobante {
 
    private Dinero valor;
    private String tienda;
 
    Comprobante(int monto, String divisa, String tienda) {
        this.valor = new Dinero(monto, divisa);
        this.tienda = tienda;
    }
 
    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Voucher))
            return false;
        Voucher otro = (Voucher) o;
        boolean valorEquals = (this.valor == null && otro.valor == null)
          || (this.valor != null && this.valor.equals(otro.valor));
        boolean tiendaEquals = (this.tienda == null && otro.tienda == null)
          || (this.tienda != null && this.tienda.equals(otro.tienda));
        return valorEquals && tiendaEquals;
    }
 
}

Y de ese modo el balance universal queda restablecido, y la simetría de equals() de Comprobante vuelve a funcionar.

hashCode()

Este método regresa un número entero que representa la instancia actual de una clase. Este valor debe ser calculado en consistencia con la definición de igualdad de la clase. por lo tanto no podemos hablar de sobrescribir el método equals() si no sobrescribimos también el método hashCode()

El contrato hashCode()

El contrato hashCode es mas simple que el de equals()

  • Consistencia interna el valor de hashCode debe cambiar solo si una propiedad de las que afecta equals() cambia
  • Consistencia de igualdad Objetos que son iguales deben devolver el mismo hashCode
  • Colisiones Objetos que no son iguales podrían devolver el mismo hashCode
@Override
public final int hashCode() {
    int resultado = 17;
    if (ciudad != null) {
        resultado = 31 * resultado + ciudad.hashCode();
    }
    if (departamento != null) {
        resultado = 31 * resultado + departamento.hashCode();
    }
    return resultado;
}

En este ejemplo vemos una implementación de hashCode que se basa en las propiedades de una clase para calcular el valor. Si dejáramos el comportamiento por defecto, entonces algunas características de algunas clases dejarían de funcionar correctamente. Como es el caso del método get de la clase HashMap. La cual utiliza el hashCode para comparar los valores dentro de sus nodos.

Conclusión

En general no debemos sobrescribir los métodos equals y hashCode, a menos que el requerimiento sea comparar campos específicos del objeto para determinar su condición de igualdad o desigualdad.

Si por el motivo anteriormente expuesto nos vemos en la necesidad de sobrescribir el método equals, obligatoriamente debemos sobrescribir el método hashCode para mantener la consistencia en algunas de las características del lenguaje.

Categorized in:

Tagged in:

, ,