1. Información general
En este tutorial, presentaremos dos métodos que están estrechamente relacionados: equals () y hashCode () . Nos centraremos en su relación entre ellos, cómo anularlos correctamente y por qué deberíamos anular ambos o ninguno.
2. es igual a ()
La clase Object define los métodos equals () y hashCode () , lo que significa que estos dos métodos se definen implícitamente en cada clase Java, incluidas las que creamos:
class Money { int amount; String currencyCode; }
Money income = new Money(55, "USD"); Money expenses = new Money(55, "USD"); boolean balanced = income.equals(expenses)
Esperaríamos que los ingresos.equals (gastos) regresen verdaderos . Pero con la clase Money en su forma actual, no lo hará.
La implementación predeterminada de equals () en la clase Object dice que la igualdad es lo mismo que la identidad del objeto. Y los ingresos y los gastos son dos instancias distintas.
2.1. Igual a prevalente ()
Anulemos el método equals () para que no considere solo la identidad del objeto, sino también el valor de las dos propiedades relevantes:
@Override public boolean equals(Object o) if (o == this) return true; if (!(o instanceof Money)) return false; Money other = (Money)o; boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
2.2. igual () Contrato
Java SE define un contrato que nuestra implementación del método equals () debe cumplir. La mayoría de los criterios son de sentido común. El método equals () debe ser:
- reflexivo : un objeto debe igualarse a sí mismo
- simétrico : x.equals (y) debe devolver el mismo resultado que y.equals (x)
- transitivo : si x.equals (y) y y.equals (z) entonces también x.equals (z)
- consistente : el valor de equals () debe cambiar solo si una propiedad que está contenida en equals () cambia (no se permite la aleatoriedad)
Podemos buscar los criterios exactos en Java SE Docs para la clase Object .
2.3. Violar es igual a () simetría con herencia
Si el criterio para igual () es de sentido común, ¿cómo podemos violarlo? Bueno, las infracciones ocurren con mayor frecuencia, si ampliamos una clase que ha anulado equals () . Consideremos una clase Voucher que amplía nuestra clase Money :
class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o) // other methods }
A primera vista, la clase Cupón y su anulación por iguales () parecen ser correctas. Y ambos métodos equals () se comportan correctamente siempre que comparemos Dinero con Dinero o Cupón con Cupón . Pero, ¿qué pasa si comparamos estos dos objetos?
Money cash = new Money(42, "USD"); WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon"); voucher.equals(cash) => false // As expected. cash.equals(voucher) => true // That's wrong.
Eso viola los criterios de simetría del contrato igual () .
2.4. Arreglar es igual a () simetría con composición
Para evitar este escollo, deberíamos privilegiar la composición sobre la herencia.
En lugar de subclasificar Money , creemos una clase Voucher con una propiedad Money :
class Voucher { private Money value; private String store; Voucher(int amount, String currencyCode, String store) { this.value = new Money(amount, currencyCode); this.store = store; } @Override public boolean equals(Object o) // other methods }
Y ahora, los iguales funcionarán simétricamente como lo requiere el contrato.
3. hashCode ()
hashCode () devuelve un número entero que representa la instancia actual de la clase. Debemos calcular este valor de acuerdo con la definición de igualdad para la clase. Por lo tanto, si anulamos el método equals () , también tenemos que anular hashCode () .
Para obtener más detalles, consulte nuestra guía de hashCode () .
3.1. hashCode () Contrato
Java SE también define un contrato para el método hashCode () . Una mirada a fondo muestra cuán estrechamente relacionados están hashCode () y equals () .
Los tres criterios en el contrato de hashCode () mencionan de alguna manera el método equals () :
- consistencia interna : el valor de hashCode () solamente puede cambiar si una propiedad que está en equals () cambios
- es igual a la coherencia : los objetos que son iguales entre sí deben devolver el mismo código hash
- colisiones : objetos desiguales pueden tener el mismo código hash
3.2. Violar la consistencia de hashCode () y equals ()
El segundo criterio del contrato de métodos hashCode tiene una consecuencia importante: si anulamos equals (), también debemos anular hashCode (). Y esta es, con mucho, la violación más extendida con respecto a los contratos de los métodos equals () y hashCode () .
Veamos un ejemplo de este tipo:
class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }
La clase Team anula solo equals () , pero aún usa implícitamente la implementación predeterminada de hashCode () como se define en la clase Object . Y esto devuelve un hashCode () diferente para cada instancia de la clase. Esto viola la segunda regla.
Ahora bien, si creamos dos objetos Team , ambos con ciudad "Nueva York" y departamento "marketing", serán iguales, pero devolverán diferentes códigos hash.
3.3. Clave HashMap con un hashCode () inconsistente
But why is the contract violation in our Team class a problem? Well, the trouble starts when some hash-based collections are involved. Let's try to use our Team class as a key of a HashMap:
Map leaders = new HashMap(); leaders.put(new Team("New York", "development"), "Anne"); leaders.put(new Team("Boston", "development"), "Brian"); leaders.put(new Team("Boston", "marketing"), "Charlie"); Team myTeam = new Team("New York", "development"); String myTeamLeader = leaders.get(myTeam);
We would expect myTeamLeader to return “Anne”. But with the current code, it doesn't.
If we want to use instances of the Team class as HashMap keys, we have to override the hashCode() method so that it adheres to the contract: Equal objects return the same hashCode.
Let's see an example implementation:
@Override public final int hashCode() { int result = 17; if (city != null) { result = 31 * result + city.hashCode(); } if (department != null) { result = 31 * result + department.hashCode(); } return result; }
After this change, leaders.get(myTeam) returns “Anne” as expected.
4. When Do We Override equals() and hashCode()?
Generally, we want to override either both of them or neither of them. We've just seen in Section 3 the undesired consequences if we ignore this rule.
Domain-Driven Design can help us decide circumstances when we should leave them be. For entity classes – for objects having an intrinsic identity – the default implementation often makes sense.
However, for value objects, we usually prefer equality based on their properties. Thus want to override equals() and hashCode(). Remember our Money class from Section 2: 55 USD equals 55 USD – even if they're two separate instances.
5. Implementation Helpers
We typically don't write the implementation of these methods by hand. As can be seen, there are quite a few pitfalls.
One common way is to let our IDE generate the equals() and hashCode() methods.
Apache Commons Lang and Google Guava have helper classes in order to simplify writing both methods.
Project Lombok also provides an @EqualsAndHashCode annotation. Note again how equals() and hashCode() “go together” and even have a common annotation.
6. Verifying the Contracts
If we want to check whether our implementations adhere to the Java SE contracts and also to some best practices, we can use the EqualsVerifier library.
Let's add the EqualsVerifier Maven test dependency:
nl.jqno.equalsverifier equalsverifier 3.0.3 test
Let's verify that our Team class follows the equals() and hashCode() contracts:
@Test public void equalsHashCodeContracts() { EqualsVerifier.forClass(Team.class).verify(); }
It's worth noting that EqualsVerifier tests both the equals() and hashCode() methods.
EqualsVerifier is much stricter than the Java SE contract. For example, it makes sure that our methods can't throw a NullPointerException. Also, it enforces that both methods, or the class itself, is final.
It's important to realize that the default configuration of EqualsVerifier allows only immutable fields. This is a stricter check than what the Java SE contract allows. This adheres to a recommendation of Domain-Driven Design to make value objects immutable.
If we find some of the built-in constraints unnecessary, we can add a suppress(Warning.SPECIFIC_WARNING) to our EqualsVerifier call.
7. Conclusion
In this article, we've discussed the equals() and hashCode() contracts. We should remember to:
- Always override hashCode() if we override equals()
- Anular equals () y hashCode () para objetos de valor
- Tenga en cuenta las trampas de las clases extendidas que han anulado equals () y hashCode ()
- Considere usar un IDE o una biblioteca de terceros para generar los métodos equals () y hashCode ()
- Considere usar EqualsVerifier para probar nuestra implementación
Finalmente, todos los ejemplos de código se pueden encontrar en GitHub.