Contratos Java equals () e hashCode ()
1. Visão geral
Neste tutorial, apresentaremos dois métodos que estão intimamente ligados:equals()ehashCode(). Vamos nos concentrar em seu relacionamento um com o outro, como substituí-los corretamente e por que devemos substituir ambos ou nenhum.
2. equals()
A classeObject define os métodosequals()ehashCode() - o que significa que esses dois métodos são definidos implicitamente em cada classe Java, incluindo as que criamos:
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 queincome.equals(expenses) retornassetrue. Mas com a classeMoney em sua forma atual, isso não acontecerá.
A implementação padrão deequals() na classeObject diz que igualdade é o mesmo que identidade de objeto. Eincome eexpenses são duas instâncias distintas.
2.1. Substituindoequals()
Vamos substituir o métodoequals() para que ele não considere apenas a identidade do objeto, mas também o valor das duas propriedades 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)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
return this.amount == other.amount && currencyCodeEquals;
}
2.2. Contrato deequals()
Java SE define um contrato que nossa implementação do métodoequals() deve cumprir. Most of the criteria are common sense. O métodoequals() deve ser:
-
reflexive: um objeto deve ser igual a si mesmo
-
symmetric:x.equals(y) must return the same result as y.equals(x)
-
transitive: sex.equals(y) ey.equals(z), então tambémx.equals(z)
-
consistent: o valor deequals() deve mudar apenas se uma propriedade que está contida emequals() mudar (nenhuma aleatoriedade permitida)
Podemos procurar os critérios exatos emJava SE Docs for the Object class.
2.3. Violandoequals() simetria com herança
Se o critério paraequals() for de bom senso, como podemos violá-lo? Bem,violations happen most often, if we extend a class that has overridden equals(). Vamos considerar uma classeVoucher que estende nossa classeMoney:
class WrongVoucher extends Money {
private String store;
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof WrongVoucher))
return false;
WrongVoucher other = (WrongVoucher)o;
boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
|| (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return this.amount == other.amount && currencyCodeEquals && storeEquals;
}
// other methods
}
À primeira vista, a classeVoucher e sua substituição paraequals() parecem estar corretas. E ambos os métodosequals() se comportam corretamente, desde que comparemosMoney aMoney ouVoucher aVoucher. But what happens, if we compare these two objects?
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.
Isso viola os critérios de simetria do contratoequals().
2.4. Fixandoequals() simetria com composição
Para evitar essa armadilha, devemos favor composition over inheritance.
Em vez de subclassificarMoney, vamos criar uma classeVoucher com uma propriedadeMoney:
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) {
if (o == this)
return true;
if (!(o instanceof Voucher))
return false;
Voucher other = (Voucher) o;
boolean valueEquals = (this.value == null && other.value == null)
|| (this.value != null && this.value.equals(other.value));
boolean storeEquals = (this.store == null && other.store == null)
|| (this.store != null && this.store.equals(other.store));
return valueEquals && storeEquals;
}
// other methods
}
E agora,equals lavará simetricamente conforme o contrato exige.
3. hashCode()
hashCode() retorna um inteiro que representa a instância atual da classe. Devemos calcular esse valor consistente com a definição de igualdade para a classe. Assim, o métodoif we override the equals(), também temos que substituirhashCode().
Para mais alguns detalhes, verifique nossoguide to hashCode().
3.1. Contrato dehashCode()
Java SE também define um contrato para o métodohashCode(). Uma análise completa mostra comohashCode() eequals() estão intimamente relacionados.
Todos os três critérios no contrato dehashCode() mencionam de alguma forma o métodoequals():
-
internal consistency: o valor dehashCode() só pode mudar se uma propriedade que está emequals() mudar
-
equals consistency: * objetos iguais entre si devem retornar o mesmo hashCode *
-
collisions:unequal objects may have the same hashCode
3.2. Violando a consistência dehashCode() eequals()
O 2º critério do contrato de métodos hashCode tem uma consequência importante:If we override equals(), we must also override hashCode(). E esta é de longe a violação mais comum em relação aos contratos dos métodosequals()ehashCode().
Vejamos um exemplo:
class Team {
String city;
String department;
@Override
public final boolean equals(Object o) {
// implementation
}
}
A classeTeam substitui apenasequals(), mas ainda usa implicitamente a implementação padrão dehashCode() conforme definido na classeObject. E isso retorna umhashCode() diferente para cada instância da classe. This violates the second rule.
Agora, se criarmos dois objetosTeam, ambos com cidade “Nova York” e departamento “marketing”, eles serão iguais,but they will return different hashCodes.
3.3. HashMap Chave com umhashCode() inconsistente
Mas por que a violação do contrato em nossa classeTeam é um problema? Bem, o problema começa quando algumas coleções baseadas em hash estão envolvidas. Vamos tentar usar nossa classeTeam como uma chave deHashMap:
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);
Esperaríamos quemyTeamLeader retornasse “Anne”. But with the current code, it doesn’t.
Se quisermos usar instâncias da classeTeam como chavesHashMap, temos que substituir o métodohashCode() para que ele adira ao contrato:Equal objects return the same hashCode.
Vejamos um exemplo de implementação:
@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;
}
Após esta alteração,leaders.get(myTeam) retorna “Anne” como esperado.
4. Quando substituímosequals() ehashCode()?
Generally, we want to override either both of them or neither of them. Acabamos de ver na Seção 3 as consequências indesejáveis se ignorarmos esta regra.
O Design Orientado por Domínio pode nos ajudar a decidir as circunstâncias em que devemos deixá-las. Para classes de entidade - para objetos com uma identidade intrínseca - a implementação padrão geralmente faz sentido.
No entanto,for value objects, we usually prefer equality based on their properties. Portanto, deseja substituirequals()ehashCode(). Lembre-se de nossa classeMoney da Seção 2: 55 USD é igual a 55 USD - mesmo que sejam duas instâncias separadas.
5. Ajudantes de implementação
Normalmente não escrevemos a implementação desses métodos manualmente. Como pode ser visto, existem algumas armadilhas.
Uma maneira comum élet our IDE gerar os métodosequals()ehashCode().
Apache Commons Lang eGoogle Guava têm classes auxiliares para simplificar a escrita de ambos os métodos.
Project Lombok também fornece uma anotação@EqualsAndHashCode. Note again how equals() and hashCode() “go together” and even have a common annotation.
6. Verificando os Contratos
Se quisermos verificar se nossas implementações estão de acordo com os contratos do Java SE e também com algumas das melhores práticas,we can use the EqualsVerifier library.
Vamos adicionar a dependência de teste MavenEqualsVerifier:
nl.jqno.equalsverifier
equalsverifier
3.0.3
test
Vamos verificar se nossa classeTeam segue os contratosequals()ehashCode():
@Test
public void equalsHashCodeContracts() {
EqualsVerifier.forClass(Team.class).verify();
}
É importante notar queEqualsVerifier testa os métodosequals()ehashCode().
EqualsVerifier is much stricter than the Java SE contract. Por exemplo, certifica-se de que nossos métodos não podem lançar umNullPointerException. Além disso, garante que ambos os métodos, ou a própria classe, sejam finais.
É importante perceber quethe default configuration of EqualsVerifier allows only immutable fields. Essa é uma verificação mais rigorosa do que o permitido pelo contrato do Java SE. Isso segue uma recomendação do Design Orientado a Domínio para tornar objetos de valor imutáveis.
Se acharmos algumas das restrições embutidas desnecessárias, podemos adicionar umsuppress(Warning.SPECIFIC_WARNING) à nossa chamadaEqualsVerifier.
7. Conclusão
Neste artigo, discutimos os contratosequals()ehashCode(). Devemos lembrar de:
-
Sempre substituahashCode() se substituirmosequals()
-
Substituirequals() ehashCode() para objetos de valor
-
Esteja ciente das armadilhas de classes de extensão que substituíramequals() ehashCode()
-
Considere usar um IDE ou uma biblioteca de terceiros para gerar os métodosequals()ehashCode()
-
Considere usar o EqualsVerifier para testar nossa implementação
Finalmente, todos os exemplos de código podem ser encontradosover on GitHub.