Olá Pessoal, tudo bom?
Hoje o post será sobre uma duvida que um leitor me fez a respeito de um código de teste de uma classe do sistema de BilhetesAereos
Duvida
O leitor Felipe Kawassaki realizou o seguinte questionamento:
Ao rodar o teste JUnit da classe TesteRotaDAOModification, o teste não ficou correto dando assertion error no método constains(), linha 48 da classe RotaVerificator após realizar a inserção da informação no banco de dados. O que poderia estar errado? Seguem as classes:
Obs Mauda: Em todas as classes deixei somente os itens relevantes para a duvida.
Classe Rota:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public class Rota implements IdentifierInterface, Serializable { @Id @GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY) private Long id; //Algumas linhas omitidas public Rota(CiaAerea ciaAerea, Aeroporto origem, Aeroporto destino) { this.ciaAerea = ciaAerea; this.ciaAerea.getRotas().add(this); this.origem = origem; this.destino = destino; } //Mais algumas linhas omitidas @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Rota other = (Rota) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; return true; } } |
Classe CiaAerea:
1 2 3 4 5 6 7 8 9 10 11 |
public class CiaAerea implements IdentifierInterface, Serializable { @Id @GeneratedValue(strategy = javax.persistence.GenerationType.IDENTITY) private Long id; @OneToMany(mappedBy="ciaAerea") private Set<Rota> rotas = new LinkedHashSet<>(); //Outras Linhas omitidas } |
Classe RotaVerificator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class RotaVerificator extends AbstractVerificator<Rota, MassaTestesRotaEnum>{ public void verify(Rota rota){ super.verify(rota); Assert.assertTrue(StringUtils.isNotBlank(rota.getNome())); Assert.assertTrue(StringUtils.isNotBlank(rota.getDescricao())); CiaAereaVerificator.getInstance().verify(rota.getCiaAerea()); AeroportoVerificator.getInstance().verify(rota.getOrigem()); AeroportoVerificator.getInstance().verify(rota.getDestino()); //Verifica a associacao bidirecional com CiaAerea Assert.assertTrue(rota.getCiaAerea().getRotas().contains(rota)); } |
Explicação sobre Equals e HashCode
O Java utiliza os métodos equals() e hashCode() para realizar a distinção entre objetos, tanto que esses métodos estão implementados na classe java.lang.Object e são amplamente utilizados em várias bibliotecas do Java, como a Collections. Na maioria das vezes esses métodos não são sobrescritos na classes Java (lembre-se que todas as classes são filhas de Object), assim utilizam as implementações originais da classe Object.
Ao criar um novo objeto, o objetivo deste é que seja diferente de outros objetos existentes. O método equals() da classe Object compara endereços de memória, logo se foi criado um outro objeto, mesmo que todos seus atributos estejam iguais a outro objeto, a comparação via equals() original indicará que os objetos são diferentes, e via memória o são.
Já quando sobrescrevemos esse método, nós devemos colocar ali atributos que façam sentido haver uma comparação lógica entre eles. Normalmente escolhemos os identificadores das entidades.
O método hashCode() retorna um inteiro que indica uma representação numérica daquele objeto dependendo dos valores existentes. Esses valores podem ser iguais para objetos diferentes, pois estamos tratando de um inteiro, um tipo com valor método, 2 elevado 32 menos 1 unidade que representa o bit negativo. Isso dá mais de 4 Bilhões. É muita coisa, mas em termos de computação não é tão grande assim. Podendo haver colisões.
Então:
- Se o método equals() retorna true para dois objetos, necessariamente o retorno do metodo hashCode() deverá ser igual.
- Se o método hashCode() retornar o mesmo valor para dois objetos não necessáriamente que o equals() retornará true para os objetos.
Explicação sobre Sets
Vamos observar o código enviado pelo leitor, ele está utilizando uma interface Set<Rota>, para armazenar todas as rotas de uma CiaAerea, utilizando a classe HashSet<Rota> para implementar essa interface. Um Set é uma estrutura de dados, conhecido como Conjunto. Ele irá armazenar vários valores não repetidos, caso ocorra um valor repetido o valor novo irá substituir o velho.
A classe HashSet utiliza-se de um HashMap interno para armazenar os valores, conforme o código abaixo:
1 2 3 4 5 6 7 |
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable { private transient HashMap<E,Object> map; //Outras linhas foram omitidas }//fim da classe HashSet |
Um HashMap é uma estrutura que armazena os valores no formato <chave, valor>. E o HashMap se utiliza do seguinte método para gerar a chave:
1 2 3 4 |
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } |
Perceba que se o objeto for válido o HashMap irá chamar o método hashCode() para obter o valor da chave para armazenar no Map. Assim, para melhor visualização, pense num Map como uma tabela com uma coluna Id e uma coluna Valor. Ao escolher determinado Id, o valor é retornado.
Descrevendo o Erro
Ao observar a classe Rota nós temos que o método hashCode() utiliza-se do id da classe Rota para estabelecer seu valor. Caso o id seja nulo, irá gerar um valor 31, caso não o seja irá gerar um valor 31 mais o hashCode do id. Como indica a documentação do Hibernate ((Hibernate API : 4.3. Implementing equals() and hashCode() – http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html_single/#persistent-classes-equalshashcode))
Furthermore, if an instance is unsaved and currently in a Set, saving it will assign an identifier value to the object. If equals() and hashCode() are based on the identifier value, the hash code would change, breaking the contract of the Set.
Além disso, se uma instancia não está salva e atualmente se encontra em um Set, salvá-la irá assinar um valor diferente ao identificador da instância. Se o método equals() e o hashCode() são baseados nesse identificaodor, o hash code irá mudar, quebrando o contrato com o Set.
No construtor de Rota, o nome objeto criado é adicionado ao set. Mas nesse momento o id de rota é nulo, logo esse novo objeto é adicionado no hash 31 do HashSet.
Após o objeto Rota ser persistido no banco de dados, o valor do id muda, o que implica que o valor do hashCode também muda. Assim como o HashSet utiliza-se do hashCode para obter a informação do Map no contains, pois no nosso caso o valor não é mais 31 e sim um valor diferente devido ao novo id, o HashSet não irá recuperar um valor e o contains retornará false, invalidando o teste.
Corrigindo o Erro
Como o valor gerado pelo hashCode é utilizado para adicionar informações ao set nós não devemos utilizar valores que podem mudar bruscamente com o tempo. Assim é interessante utilizar de valores que sejam como chaves naturais ao objeto, pois caso essas mudem o objeto perde o contexto.
Assim uma solução para o problema seria escolher outros valores para o hashCode() de Rota. Valores que fossem intrínsecos a classe Rota. Um ponto interessante é utilizar atributos que sejam únicos em sua comparação e que já estejam persistidos no banco de dados, para não causar novos erros com o método contains().
No caso da classe Rota, os atributos ciaAerea, aeroportoOrigem e aeroportoDestino são atributos utéis para esse caso, pois já estão persistidos no banco de dados e, caso mudem, fazem com que a Rota seja realmente um novo objeto, pois não teria relação com a Rota anterior. Logo o método hashCode() deverá ficar com a seguinte codificação:
1 2 3 4 5 6 7 |
@Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ciaAerea.hashCode() + origem.hashCode() + destino.hashCode(); return result; } |
Lembrando que a classe CiaAerea e Aeroporto devem implementar seus próprios hashCode() e equals().
finally{
Caso você tem maiores duvidas a respeito dessa explicação, por favor deixe nos comentários abaixo!
Duvidas ou sugestões? Deixe seu feedback! Isso ajuda a saber a sua opinião sobre os artigos e melhorá-los para o futuro! Isso é muito importante!
Até um próximo post!
Leave a Reply