• Revista PROGRAMAR: Já está disponível a edição #53 da revista programar. Faz já o download aqui!

Nazgulled

HashSet está a repetir-me objectos diferentes com valores iguais

26 mensagens neste tópico

Segundo percebi, o HashSet não deixa duplicar objectos. Mas segundo algo que encontrei na net, ele chama o método equals() do tipo de objectos passado para o HashSet para verificar se o objecto já existe. Ele faz mesmo isso? É que eu tenho aqui um exemplo que não está a funcionar...

public class Ficha {
private String nome;

public Ficha(String nome) {
	this.nome = nome;
}

public String getNome() {
	return this.nome;
}

public boolean equals(Ficha ficha) {
	if(ficha == this) return true;
	if(ficha == null) return false;

	if(!this.nome.equals(ficha.getNome())) return false;

	return true;
}

public String toString() {
	StringBuilder str = new StringBuilder();

	str.append("[ NOME: ");
	str.append(this.nome);
	str.append(" ]");

        return str.toString();
}
}

public class Teste {
public static void main(String args[]) {
	HashSet<Ficha> fichas = new HashSet<Ficha>();

	Ficha f1 = new Ficha("Tiago");
	Ficha f2 = new Ficha("Sofia");

	fichas.add(f1);
	fichas.add(f2);
	fichas.add(f1);

	Ficha f3 = new Ficha("Sofia");

	fichas.add(f3);

	System.out.println(fichas.toString());
}
}

O output deste código é:

[[ NOME: Sofia ], [ NOME: Sofia ], [ NOME: Tiago ]]

Se o HashSet estivesse a usar o equals() da classe Ficha, apenas deveria ter uma Sofia e não duas...

Estou a fazer alguma mal ou é mesmo assim e não há volta a dar?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

O método equals tem de receber um object ou então não estás a redefinir o método correcto.

Atenção às assinaturas dos métodos.

Implementação alternativa:

public boolean equals(Object obj) {
    if(ficha == this) return true;

    if(!(obj instanceof Ficha) return false;
    
    Ficha f = (Ficha) obj;
    return this.nome.equals(f.nome);    
}

Uma correcção, não vale a pena verificares se o objecto passado é null, ao fazeres obj instanceof Ficha, se obj for null então o resultado é falso.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Esqueci-me, no meu código também tenho uma redefinição assim:

public boolean equals(Object object) {
if(object == this) return true;
if(object == null) return false;

if (this.getClass() != object.getClass()) return false;

return this.equals((Ficha)object);
}

E continuou a não dar...

Também tentei com o teu código, após substituir ficha == this por obj == this e adicionar um parêntesis final ao segundo if se não o código não compilava e, mesmo assim não deu. O output foi o mesmo...

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

O teu não dá porque estás a invocar o equals de Object, na última instrução.

Sendo X uma classe, o método equals ficaria:

public boolean equals(Object obj) {
        if (this==obj) return true;
        if ((obj==null) || (this.getClass()!=obj.getClass())) return false;
        X p = (X)obj;
        return (algoritmo de comparação...);
    }

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Não lês-te o meu último post... :D

Estava aqui a fazer debug e cheguei a uma conclusão... Algures quando ele esta a adicionar uma nova ficha ao HashSet, ele executa este pedaço de código:

public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

E como podem ver, existe ali um key.equals(k) o qual não nem se quer é invocado... O que me parece é que ele nem entra no for.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Também tentei com o teu código, após substituir ficha == this por obj == this e adicionar um parêntesis final ao segundo if se não o código não compilava e, mesmo assim não deu. O output foi o mesmo...

Desculpa a parte dos erros fiz o código sem ter olhado muito.

O teu não dá porque estás a invocar o equals de Object, na última instrução.

Isso não implicaria erro nenhum, a única coisa que poderia causar problemas seria o facto de o método não estar implementado, mas dado que neste caso é uma string, não seria um problema.

Tanto o método equals do elemento como o da chave têm de estar implementados. Não sei que chave estás a tentar usar mas se for um objecto teu terás de implementar o método.

Claro que o problema poderá estar noutra parte mas dado o código que colocaste não dá para ver muito.

Outra coisa, não te recomendo teres mais que um método equals, um basta e por vezes mais que um pode causar problemas porque nem sempre sabes qual vai ser chamado.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Eu estou a programar tal e qual como o prof quer que a gente o faça, por isso vou deixar o código como está. E além do mais, da forma como eles estão implementados, não vejo qualquer problema que possa existir independentemente de qual dos métodos for chamado.

Quanto ao problema, disseram-me que tinha que implementar o hashCode também. Foi isso que disseste Knitter? Como o faço? Podem dar ai um exemplo?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Por acaso tinha esquecido o hashcode, e sim, ele também precisa ser implementado.

O hashcode deve ser implementado usando os valores que são usados na comparação.

Este é um exemplo de um par de equals/hash que retirei do meu projecto, por isso ignora o que te pareça a mais :D. Nota apenas que os campos usados no equals são usados no hash. Os valores inteiros são valores aleatórios.

    /**
     * Equals method doesn't take into account the id of the object. Hibernate 
     * only gives an id to a new object when that object is persisted, it is 
     * therefor not possible to use the id attribute.
     * 
     * A <em>Bussines Key</em> is used instead.
     * @param obj the object to compare
     * @return true if both objects are equal under the <em>Bussines Key</em>
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Media)) {
            return false;
        }

        Media other = (Media) obj;
        return name.equals(other.name) && description.equals(other.description) && type == other.type;
    }

    /**
     * HashCode for this object, excluding the id field.
     * @return integer representing the hashcode
     * 
     * @see equals
     */
    @Override
    public int hashCode() {
        int hash = 7;
        hash = 37 * hash + (this.name != null ? this.name.hashCode() : 0);
        hash = 37 * hash + (this.description != null ? this.description.hashCode() : 0);
        hash = 37 * hash + this.type;
        return hash;
    }

Se estiveres a usar o Netbeans e implementares o método equals, ele sugere-te que implementes o hash e até te cria o código automaticamente usando os campos certos. Se não tiveres o método equals podes usar o opção para inserir código e tens lá a opção para criar o hash e escolher os campos a usar.

Se tens dois métodos iguais então um deles não serve para nada, se por acaso um deles tiver alguma diferença, então, dado que não sabes qual dos dois é invocado, podes ter resultados diferentes na mesma situação.

Porque é que precisas de dois métodos?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Resumindo, estas a implementar um método que devolve um hashcode conforme os valores das variáveis de instÂncia de tal forma que seja igual se todos os valores forem iguais entre vários objectos, correcto?

Só não estou a perceber aquele hash = 7 e depois hash = 37 * hash. É mesmo necessário?

Não poderia fazer algo mais simples como somar o hashCode() de cada uma das variáveis de instância (supondo que nenhuma delas está vazia)?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites
Não sendo fácil criar "boas" soluções de hashCode() para os nossos objectos, existem no entanto algumas regras simples que conduzem, em geral, a bons resultados:

- Na criação do hashcode usar valores das variáveis de instância que são relevantes no objecto, isto é, mais identificativas e deferenciadoras;

- Para variáveis de instância de tipos como String, Integer, etc., pode ser usado o próprio método hashCode() destas classes;

- Nos cálculos que conduzem à criação do hashcode único, usar pesos que sejam números primos.

Para variáveis de tipo referenciado, se o seu valor for null o seu hashValue deverá ser 0, senão, deverá invocar-se o respectivo hashCode().

Acho que é daquela regra que vem o 7 e o 37.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Não poderia fazer algo mais simples como somar o hashCode() de cada uma das variáveis de instância (supondo que nenhuma delas está vazia)?

É uma questão de produzir melhores valores na função de hash.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Usar um HashSet, tendo os elementos essa função de hash, é simplesmente ridículo...

EDIT: Já agora, se seguires à risca as regras de encapsulamento que o MM ensina em POO, nunca terás o problema de que ele fala, pois quando introduzires um valor no HashSet, não introduzes o valor, mas uma cópia.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

@Baderous

Não percebi a primeira regra e já agora, de onde tiraste isso? Do livro do prof?

A primeira regra aplica-se, por ex, se tiveres um Ponto descrito por um X, um Y e um boolean que indica se é meu ou teu (lol). É mais lógico usar o hashCode do X e do Y para calcular o hashCode final, visto que essas 2 variáveis são mais identificadoras do objecto. Tirei do livro dele, só tem 1 página sobre isto. :x

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Pronto, então no meu exemplo real (que é as fichas de tripulação do trabalho que vocês sabem qual é), tenho o seguinte:

public int hashCode() {
	int hash = this.funcao.hashCode();

        hash = 5 * hash + (this.nome != null ? this.nome.hashCode() : 0);
        hash = 5 * hash + (this.nacionalidade != null ? this.nacionalidade.hashCode() : 0);

        return hash;
}

Gosto mais do valor 5 ;), está bem ou veem algum problema nisto? Tendo em consideração as regras de encapsulamento que o MM ensina como disse o Rui Carlos, não vou ter problemas?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Resumindo, estas a implementar um método que devolve um hashcode conforme os valores das variáveis de instÂncia de tal forma que seja igual se todos os valores forem iguais entre vários objectos, correcto?

Sim.

Só não estou a perceber aquele hash = 7 e depois hash = 37 * hash. É mesmo necessário?

Não poderia fazer algo mais simples como somar o hashCode() de cada uma das variáveis de instância (supondo que nenhuma delas está vazia)?

A parte do 7 é para devolver um número primo se todos os outros elementos forem zero.

Mas sim, poderias ter como indicaste, mas isso resultaria num código menos bom.

Onde o gajo no final simplesmente sugere a fazer isto:

public int hashCode() { return 0; }

O que têm a dizer sobre isto?

Não fui ler o texto, mas pelo que citaste, diria que para qualquer instância que ele vai criar o hash é sempre o mesmo, ora isso difere um pouco do objectivo de um código de hash, e derrota a vantagem de ter um hash para cada instância, bem como produz resultados estranhos em alguns métodos que se baseiem em hash para identificar o objecto.

Claro que dependerá do objectivo, mas à primeira vista diria que essa opção é errada. Faz-me até pensar que código colocaria ele em classes diferentes? É que se fizer o mesmo código, então duas instâncias de duas classes diferentes irão ser identificadas como sendo o mesmo objecto.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Pronto, então no meu exemplo real (que é as fichas de tripulação do trabalho que vocês sabem qual é), tenho o seguinte:

public int hashCode() {
	int hash = this.funcao.hashCode();

        hash = 5 * hash + (this.nome != null ? this.nome.hashCode() : 0);
        hash = 5 * hash + (this.nacionalidade != null ? this.nacionalidade.hashCode() : 0);

        return hash;
}

Gosto mais do valor 5 ;), está bem ou veem algum problema nisto? Tendo em consideração as regras de encapsulamento que o MM ensina como disse o Rui Carlos, não vou ter problemas?

Por acaso vais andar a editar os valores que estão no HashSet? Não me parece, por isso não terás problemas. Caso precisasses de editar, só tinhas que remover o valor, e voltar a colocá-lo no HashSet.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

A parte do 7 é para devolver um número primo se todos os outros elementos forem zero.

Mas sim, poderias ter como indicaste, mas isso resultaria num código menos bom.

Em quê que o meu código se torna menos bom que o teu?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Nos cálculos que conduzem à criação do hashcode único, usar pesos que sejam números primos.

Não poderia fazer algo mais simples como somar o hashCode() de cada uma das variáveis de instância (supondo que nenhuma delas está vazia)?

Neste caso, se não garantires a existência de um peso que seja número primo.

Não estava a responder ao código que colocaste mas sim à pergunta.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Ah.... então o meu código como usa ali o 5, é primo, logo tá bom?

E já agora, será que aquele if == null é mesmo preciso?

As minhas variáveis de instância nunca serão nulas, no máximo serão vazias, mas nulas não me parece. Porque o input será sempre validado antes de ser inserido, logo, se o utilizador não inserir nada, eu nem se quer vou criar o objecto (do tipo respectivo).

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Dizem os entendidos que os pesos primos são bons para códigos de hash, nunca tentei discutir com essa noção ;), e portanto digo-te o que eles dizem dado que neste caso superam o meu conhecimento.

Se realmente consegues garantir que *nunca* tens um null, sim não vejo problema em retirares essa verificação, também não vejo porque não possa ficar.

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Código desnecessário que não está lá a fazer nada... ;)

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

    /**
     * Equals method doesn't take into account the id of the object. Hibernate 
     * only gives an id to a new object when that object is persisted, it is 
     * therefor not possible to use the id attribute.
     * 
     * A <em>Bussines Key</em> is used instead.
     * @param obj the object to compare
     * @return true if both objects are equal under the <em>Bussines Key</em>
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (this == obj) {
            return true;
        }

        if (!(obj instanceof Media)) {
            return false;
        }

        Media other = (Media) obj;
        return name.equals(other.name) && description.equals(other.description) && type == other.type;
    }

   }

Knitter, relativamente a este método, estás a usar o instanceof em vez de getClass(). Eu li isto:

1102305799.jpg

O que tens a dizer?

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

O que tens a dizer?

1) Existe um pequeno erro no texto :P

2) Me obrigaste a rever o documento  "The Java Language Specification"

3) Como em tudo, nada é preto e branco, na maior parte das vezes as coisas são bastantes cinzentas.

Tentando explicar, nenhum dos dois métodos é o melhor, vai depender muito do que estás a fazer.

Em minha defesa, devo referir que o exemplo que referi é retirado de um classe para a qual é possível criar subclasses, logo, à luz do texto que colocaste, getClass ou instanceof  resulta no mesmo, sendo que, para os mais preocupados com performance, o instanceof poderá ser mais rápido.

Além de que esse método que coloquei usa uma Bussines Key e não uma comparação natural - acho que devia ter escolhido outro exemplo :P

Mas vamos ao verdadeiro problema, assumindo que possuimos a classe Ponto com duas subclasses, Ponto2D e Ponto3D:

O contracto do método equals define que a sua implementação tem de ser:

  • reflexive: for any non-null reference value x, x.equals(x) should return true.
  • symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
  • transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
  • consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
  • For any non-null reference value x, x.equals(null) should return false.

Assim, a utilização do instanceof, torna bastante difícil implementar um método que cumpra este contracto, principalmente nos pontos 2 e 3. Dado que não conseguimos prever que subclasses vão ser criadas. Mas, não podemos olhar apenas para o contracto do método equals, teremos de ter em consideração outros princípios de programação OO.

Uma implementação do método equals nas subclasses, Ponto2D e Ponto3D, usando instanceof, vai permitir apenas que lhe passemos outras instâncias dessas subclasses, assumindo que essas classes não possuem também elas subclasse - estou a simplificar para poder explicar. Isto porque outras classes não passarão na comparação. Usando getClass, ou a minha opção preferida nesses casos, o atributo estático Ponto2D.class/Ponto3D.class, o resultado será o mesmo em quase todas as situações[1] - já ilaboro sobre este ponto.

Se estivermos a implementar o equals da classe Ponto, poderemos ter problemas quer lhe passemos um Ponto2D quer um Ponto3D, porque ambos vão passar no teste do instanceof, mas será que temos realmente um problema? Como referi vai ser complicado cumprir o contracto mas não é impossível. Por outro lado sabemos que um Ponto2D é efectivamente um Ponto, assim, se compararmos os atributos comuns, apenas da classe Ponto, não poderemos dizer que são dois objectos iguais?

A questão é que não só podemos, como devemos, é o princípio base da substituição em POO, uma subclasse deve poder substituir, em todas as situações, a sua superclasse. Se usarmos o getClass podes ver que essa regra vai falhar, deixamos de considerar que um filho consegue substituir um pai. Este é o principio de substituição de Liskov[2], que não conseguimos cumprir com o uso de getClass.

Também quebramos o principio de Least Astonishment[3], embora este não seja especifico de POO, é sempre bom te-lo em conta.

[1]Voltando ao ponto de que o getClass vai funcionar em quase todas as situações: Um objecto em Java é representado por um outro objecto, uma instância de Class, que é o que o método getClass te devolve. Em quase todas as situações, para os objectos que carregamos, existe apenas uma instância do objecto Class que representa a nossa classe carregada. Sei que pode ser confuso, vamos tentar exemplificar: ao instanciarmos um objecto do tipo Ponto2D,

(...)
private Ponto2D p = new Ponto2D()
(...)

a JVM vai criar uma instância de Class que representa a nossa classe Ponto2D carregada, na verdade via ser o ClassLoader da JVM que o vai fazer. Daqui para a frente, sempre que usares o método getClass, ou o atributo Ponto2D.class, obtens esta referência especial que foi criada. Uma mesma classe não pode ser carregada duas vezes pelo mesmo ClassLoader logo existirá apenas uma referência em memória para o objecto Class que representa a nossa Ponto2D. Mas o que acontecerá se dois ClassLoaders diferentes carregarem a mesma classe? Quantas referências existirão?

Neste caso vais ter duas instâncias do objecto Class que representa o nosso Ponto2D, logo, na mesma JVM, ao executares o método getClass, vais obter dois valores diferentes, dependendo da instância de Ponto2D onde invocares o método. Como o método equals da classe Class, apenas compara referências, se acontecer que dois objectos de Ponto2D sejam instanciados de classes carregadas por dois ClassLoaders diferentes, pode acontecer que a comparação falhe.

O instanceof garante que esta situação não acontece.

Considerando uma hash table com objectos Ponto, se usares o getClass no equals, e passares um Ponto2D ao método get não vais conseguir obter uma instância, mesmo que no interior exista um Ponto2D que tenha os mesmos atributos.

Um exemplo que pode ser fácil de pensar será, teres a classe Homem, e as subclasses Masculino e Feminino. Qualquer Masculino é um Homem, e qualquer Masculino deverá substituir um Homem, onde quer que seja, mas se usares o método getClass na classe Homem, nenhum Masculino poderá substituir um Homem porque nunca serão iguais.

Resumindo, depende do que queres fazer e do tipo de coisas que estás disposto a sacrificar.

Com instanceof, tens mais dificuldade em garantir o contacto do método equals mas cumpres muito mais a nível de POO. Com o método getClass consegues cumprir o contracto mas vais perder noutros pontos.

Pessoalmente uso mais o instanceof, além de que quando é importante, quando estou a redefinir o equals de uma classe que já o redefiniu, faço sempre um acesso ao equals da superclasse e tento sempre fazer de forma a garantir o contracto do método, por vezes não compensa o esforço e uso o atributo estático class das classes que quero comparar.

[2]http://en.wikipedia.org/wiki/Liskov_substitution_principle

[3]http://www.faqs.org/docs/artu/ch11s01.html e http://c2.com/cgi/wiki?PrincipleOfLeastAstonishment

1

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Percebi bem o dilema "Contracto do equals() vs Princípio da substituição". Se por um lado uma instância de Homem só pode ser igual a outra instância de Homem e não a uma instância da sua subclasse Masculino, a qual extende a sua definição, por outro, uma instância de Masculino poderá substituir uma instância de Homem, segundo o princípio da substituição. Se por um lado podemos cumprir o contracto do equals() através de getClass(), garantindo que apenas duas instâncias da mesma classe serão iguais, podendo, em determinadas ocasiões (na situação de 2 ClassLoaders), essa garantia falhar, por outro podemos não o cumprir na sua totalidade, utilizando instanceof, que não garante que se tratam de 2 instâncias da mesma classe, mas que não gera problemas ao nível dos ClassLoaders.

Muito bem explicado! :P

0

Partilhar esta mensagem


Link para a mensagem
Partilhar noutros sites

Crie uma conta ou ligue-se para comentar

Só membros podem comentar

Criar nova conta

Registe para ter uma conta na nossa comunidade. É fácil!


Registar nova conta

Entra

Já tem conta? Inicie sessão aqui.


Entrar Agora