Apache Commons é igual ao construtor hashCode [fechado]

155

Estou curioso para saber, o que as pessoas aqui pensam sobre o uso de org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder para implementar o equals/ hashCode? Seria uma prática melhor do que escrever a sua própria? Toca bem com o Hibernate? Qual a sua opinião?

aug70co
fonte
16
Apenas não fique tentado pelas funções reflectionEqualse reflectionHashcode; o desempenho é um assassino absoluto.
22411 skaffman
14
Vi algumas discussões aqui sobre iguais ontem e tive algum tempo livre, então fiz um teste rápido. Eu tinha 4 objetos com diferentes implementações iguais. anotações geradas por eclipse, equalsbuilder.append, equalsbuilder.reflection e pojomatic. A linha de base foi eclipse. equalsbuilder.append levou 3,7x. pojomatic levou 5x. a reflexão levou 25,8x. Foi bastante desanimador, porque gosto da simplicidade da reflexão e não suporto o nome "pojomatic".
Digitaljoel
5
Outra opção é o Projeto Lombok; ele usa a geração de bytecode em vez de reflexão, portanto deve ter um desempenho tão bom quanto o gerado pelo Eclipse. projectlombok.org/features/EqualsAndHashCode.html
Miles

Respostas:

212

Os construtores comuns / lang são ótimos e eu os uso há anos sem sobrecarga perceptível de desempenho (com e sem hibernação). Mas, como Alain escreve, o caminho da goiaba é ainda melhor:

Aqui está uma amostra de Bean:

public class Bean{

    private String name;
    private int length;
    private List<Bean> children;

}

Aqui estão equals () e hashCode () implementados com o Commons / Lang:

@Override
public int hashCode(){
    return new HashCodeBuilder()
        .append(name)
        .append(length)
        .append(children)
        .toHashCode();
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return new EqualsBuilder()
            .append(name, other.name)
            .append(length, other.length)
            .append(children, other.children)
            .isEquals();
    } else{
        return false;
    }
}

e aqui com Java 7 ou superior (inspirado no Guava):

@Override
public int hashCode(){
    return Objects.hash(name, length, children);
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return Objects.equals(name, other.name)
            && length == other.length // special handling for primitives
            && Objects.equals(children, other.children);
    } else{
        return false;
    }
}

Nota: este código referenciou o Guava originalmente, mas, como os comentários apontaram, essa funcionalidade foi introduzida no JDK; portanto, o Guava não é mais necessário.

Como você pode ver, a versão do Guava / JDK é mais curta e evita objetos auxiliares supérfluos. No caso de iguais, ele permite até um curto-circuito na avaliação se uma Object.equals()chamada anterior retornar falsa (para ser justo: commons / lang tem um ObjectUtils.equals(obj1, obj2)método com semântica idêntica que poderia ser usada em vez de EqualsBuilderpermitir um curto-circuito como acima).

Então: sim, os commons lang builders são muito preferíveis aos métodos equals()e hashCode()métodos construídos manualmente (ou esses monstros horríveis que o Eclipse irá gerar para você), mas as versões Java 7+ / Guava são ainda melhores.

E uma observação sobre o Hibernate:

tenha cuidado ao usar coleções lentas em suas implementações equals (), hashCode () e toString (). Isso falhará miseravelmente se você não tiver uma sessão aberta.


Nota (sobre igual a ()):

a) nas duas versões de equals () acima, convém usar um ou ambos os atalhos também:

@Override
public boolean equals(final Object obj){
    if(obj == this) return true;  // test for reference equality
    if(obj == null) return false; // test for null
    // continue as above

b) dependendo da sua interpretação do contrato equals (), você também pode alterar a (s) linha (s)

    if(obj instanceof Bean){

para

    // make sure you run a null check before this
    if(obj.getClass() == getClass()){ 

Se você usa a segunda versão, provavelmente também deseja chamar super(equals())dentro do seu equals()método. As opiniões diferem aqui, o tópico é discutido nesta pergunta:

maneira correta de incorporar a superclasse em uma implementação do Guava Objects.hashcode ()?

(embora seja sobre hashCode(), o mesmo se aplica a equals())


Nota (inspirada em Comentário de kayahr )

Objects.hashCode(..)(assim como o subjacente Arrays.hashCode(...)) pode ter um desempenho ruim se você tiver muitos campos primitivos. Nesses casos, EqualsBuilderpode realmente ser a melhor solução.

Sean Patrick Floyd
fonte
34
O mesmo será possível com o Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Thomas Jung
3
Se eu estiver lendo corretamente, Josh Bloch diz no Java Efetivo , Item 8, que você não deve usar getClass () no seu método equals (); você deveria usar instanceof.
9118 Jeff Olson
6
@SeanPatrickFloyd O caminho da goiaba não apenas cria um objeto de matriz para as variáveis, mas também converte TODOS os parâmetros em objetos. Portanto, quando você passar 10 valores int para ele, você terminará com 10 objetos Inteiros e um objeto de matriz. A solução commons-lang cria apenas um único objeto, independentemente de quantos valores você acrescenta ao código hash. O mesmo problema com equals. A goiaba converte todos os valores em objetos, o commons-lang cria apenas um único novo objeto.
kayahr
1
@wonhee Eu discordo totalmente que isso é melhor. Usar o Reflection para calcular códigos de hash não é algo que eu faria. A sobrecarga de desempenho provavelmente é insignificante, mas parece errada.
Sean Patrick Floyd
1
@kaushik fazendo uma final classe realmente resolve os problemas potenciais de ambas as versões (instanceof e getClass ()), contanto que você implementar seus iguais () em classes folha única
Sean Patrick Floyd
18

Gente, acorde! Desde o Java 7, existem métodos auxiliares para iguais e hashCode na biblioteca padrão. Seu uso é totalmente equivalente ao uso de métodos Guava.

Mikhail Golubtsov
fonte
a) no momento em que essa pergunta foi feita, o Java 7 ainda não estava lá b) tecnicamente, eles não são exatamente equivalentes. O jdk tem o método Objects.equals versus os métodos Objects.equal da Guava. Eu posso usar importações estáticas apenas na versão do Guava. São apenas cosméticos, eu sei, mas isso torna os não-goiaba visivelmente mais confusos.
Sean Patrick Floyd
Este não é um bom método para substituir um método de objetos iguais, devido ao fato de que Objects.equals chamará o método .equals da instância. Se você chamar Objects.equals no método .equals da instância, isso causará um estouro de pilha.
dardo
Você pode dar um exemplo, quando cai em um loop?
Mikhail Golubtsov 6/06/19
O OP está pedindo para substituir o método equals () dentro de um Object. De acordo com a documentação do método estático Objects.equals (): "Retorna true se os argumentos forem iguais e false, caso contrário. Consequentemente, se os dois argumentos forem nulos, true será retornado e se exatamente um argumento for nulo, false será retornado. caso contrário, a igualdade é determinada usando o método do primeiro argumento é igual. "Portanto, se você usou Objects.equals () dentro dos iguais exemplo substituídos () que ia chamar seu método próprios iguais, então Objects.equals () depois, novamente, dando um estouro de pilha.
dardo
@dardo Estamos falando sobre a implementação da igualdade estrutural, então isso significa que dois objetos são iguais um ao outro, se seus campos existirem . Veja o exemplo do Guava acima, como iguais são implementados.
Mikhail Golubtsov 15/06
8

Se você não deseja depender de uma biblioteca de terceiros (talvez esteja executando um dispositivo com recursos limitados) e nem mesmo deseja digitar seus próprios métodos, também pode permitir que o IDE faça o trabalho, por exemplo, no uso de eclipse

Source -> Generate hashCode() and equals()...

Você receberá um código 'nativo', que poderá ser configurado como desejar e que deverá ser suportado nas alterações.


Exemplo (eclipse Juno):

import java.util.Arrays;
import java.util.List;

public class FooBar {

    public String string;
    public List<String> stringList;
    public String[] stringArray;

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((string == null) ? 0 : string.hashCode());
        result = prime * result + Arrays.hashCode(stringArray);
        result = prime * result
                + ((stringList == null) ? 0 : stringList.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FooBar other = (FooBar) obj;
        if (string == null) {
            if (other.string != null)
                return false;
        } else if (!string.equals(other.string))
            return false;
        if (!Arrays.equals(stringArray, other.stringArray))
            return false;
        if (stringList == null) {
            if (other.stringList != null)
                return false;
        } else if (!stringList.equals(other.stringList))
            return false;
        return true;
    }

}
FrVaBe
fonte
14
Verdade, mas o código gerado pelo Eclipse é ilegível e impossível de manter.
Sean Patrick Floyd
6
Por favor, nunca pense em algo tão terrível quanto o gerado pelo eclipse equals. Se você não quiser depender da biblioteca de terceiros, escreva o método de uma linha como Objects.equalvocê. Mesmo quando usado apenas uma ou duas vezes, torna o código muito melhor!
Maaartinus 21/07
@maaartinus equals/ hashCodemétodos de uma linha ???
precisa saber é
1
@maaartinus Guava é uma biblioteca de terceiros. Eu indiquei que minha solução pode ser usada se você deseja EVITAR usando bibliotecas de terceiros.
precisa saber é
1
@FrVaBe: E eu escrevi "Se você não quer depender da biblioteca de terceiros, escreva o método de uma linha como Objects.equal yourself." E então eu escrevi o método de uma linha que você pode usar para evitar o uso do Goiaba e ainda reduzir o comprimento de iguais para cerca da metade.
maaartinus
6

O EqualsBuilder e o HashCodeBuilder têm dois aspectos principais diferentes do código escrito manualmente:

  • manipulação nula
  • criação de instância

O EqualsBuilder e o HashCodeBuilder facilitam a comparação de campos que poderiam ser nulos. Com o código gravado manualmente, isso cria muitos clichês.

O EqualsBuilder, por outro lado, criará uma instância por chamada de método igual. Se seus métodos iguais forem chamados com frequência, isso criará muitas instâncias.

Para o Hibernate, a implementação equals e hashCode não faz diferença. Eles são apenas um detalhe de implementação. Para quase todos os objetos de domínio carregados com hibernação, a sobrecarga de tempo de execução (mesmo sem análise de escape) do Builder pode ser ignorada . A sobrecarga do banco de dados e da comunicação será significativa.

Como skaffman mencionou, a versão de reflexão não pode ser usada no código de produção. A reflexão será lenta e a "implementação" não estará correta para todas, exceto as classes mais simples. Levar todos os membros em consideração também é perigoso, pois os membros recém-introduzidos alteram o comportamento do método igual. A versão de reflexão pode ser útil no código de teste.

Thomas Jung
fonte
Discordo que a implementação da reflexão "não será correta para todas, exceto para as classes mais simples". Com os construtores, você pode excluir explicitamente os campos, se desejar, para que a implementação realmente dependa da sua definição de chave de negócios. Infelizmente, não posso discordar do aspecto de desempenho da implementação baseada na reflexão.
Digitaljoel
1
@digitaljoel Sim, você pode excluir campos, mas essas definições não estão refatorando o salvamento. Então eu não os mencionei de propósito.
Thomas Jung
0

Se você está apenas lidando com o bean de entidade em que id é uma chave primária, você pode simplificar.

   @Override
   public boolean equals(Object other)
   {
      if (this == other) { return true; }
      if ((other == null) || (other.getClass() != this.getClass())) { return false; }

      EntityBean castOther = (EntityBean) other;
      return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
   }
DEREK LEE
fonte
0

Na minha opinião, ele não funciona bem com o Hibernate, especialmente os exemplos da resposta que compara comprimento, nome e filhos para alguma entidade. O Hibernate recomenda usar a chave comercial para ser usada em equals () e hashCode (), e eles têm seus motivos. Se você usar o gerador automático equals () e hashCode () em sua chave comercial, tudo bem, apenas os problemas de desempenho precisam ser considerados como mencionado anteriormente. Mas as pessoas geralmente usam todas as propriedades, o que está muito errado na IMO. Por exemplo, atualmente estou trabalhando em um projeto em que as entidades são escritas usando Pojomatic com @AutoProperty, o que considero um padrão realmente ruim.

Seus dois cenários principais para usar hashCode () e equals () são:

  • quando você coloca instâncias de classes persistentes em um conjunto (a maneira recomendada de representar associações com muitos valores) e
  • quando você usa o recolocação de instâncias desanexadas

Então, vamos assumir que nossa entidade se parece com isso:

class Entity {
  protected Long id;
  protected String someProp;
  public Entity(Long id, String someProp);
}

Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");

Ambas são a mesma entidade para o Hibernate, que foram buscadas em alguma sessão em algum momento (seu ID e classe / tabela são iguais). Mas quando implementamos auto equals () um hashCode () em todos os objetos, o que temos?

  1. Quando você coloca a entidade2 no conjunto persistente onde a entidade1 já existe, isso será colocado duas vezes e resultará em exceção durante a confirmação.
  2. Se você deseja anexar a entidade desanexada2 à sessão, onde a entidade1 já existe, elas (provavelmente não testei isso especialmente) não serão mescladas corretamente.

Portanto, para o projeto de 99% que faço, usamos a seguinte implementação de equals () e hashCode () escritos uma vez na classe de entidade base, o que é consistente com os conceitos do Hibernate:

@Override
public boolean equals(Object obj) {
    if (StringUtils.isEmpty(id))
        return super.equals(obj);

    return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}

@Override
public int hashCode() {
    return StringUtils.isEmpty(id)
        ? super.hashCode()
        : String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}

Para a entidade transitória, faço o mesmo que o Hibernate fará no passo de persistência, ie. Eu uso a correspondência de instância. Para os objetos persistentes, comparo a chave exclusiva, que é a tabela / ID (nunca uso chaves compostas).

Lukasz Frankowski
fonte
0

Caso outras pessoas achem útil, eu criei essa classe Helper para o cálculo do código de hash que evita a sobrecarga adicional de criação de objeto mencionada acima (na verdade, a sobrecarga do método Objects.hash () é ainda maior quando você tem herança, pois criará uma nova matriz em cada nível!).

Exemplo de uso:

public int hashCode() {
    return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}

public int hashCode() {
    return HashCode.hash(super.hashCode(), occupation, children);
}

O auxiliar HashCode:

public class HashCode {

    public static int hash(Object o1, Object o2) {
        return add(Objects.hashCode(o1), o2);
    }

    public static int hash(Object o1, Object o2, Object o3) {
        return hash(Objects.hashCode(o1), o2, o3);
    }

    ...

    public static int hash(Object o1, Object o2, ..., Object o10) {
        return hash(Objects.hashCode(o1), o2, o3, ..., o10);
    }

    public static int hash(int initial, Object o1, Object o2) {
        return add(add(initial, o1), o2);
    }

    ...

    public static int hash(int initial, Object o1, Object o2, ... Object o10) {
        return add(... add(add(add(initial, o1), o2), o3) ..., o10);
    }

    public static int hash(long value) {
        return (int) (value ^ (value >>> 32));
    }

    public static int hash(int initial, long value) {
        return add(initial, hash(value));
    }

    private static int add(int accumulator, Object o) {
        return 31 * accumulator + Objects.hashCode(o);
    }
}

Imaginei que 10 é o número máximo razoável de propriedades em um modelo de domínio; se você tiver mais, pense em refatorar e introduzir mais classe em vez de manter um monte de Strings e primitivas.

As desvantagens são: não é útil se você tiver principalmente primitivos e / ou matrizes que você precisa fazer hash profundo. (Normalmente, esse é o caso quando você precisa lidar com objetos simples (transferência) que estão fora de seu controle).

Vlad
fonte