Entrar na área cinzenta de "tópico ligado / desligado", mas necessário para eliminar a confusão sobre a sugestão de Oscar Reyes de que mais colisões de hash é uma coisa boa porque reduz o número de elementos no HashMap. Posso entender mal o que Oscar está dizendo, mas não pareço ser o único: kdgregory, delfuego, Nash0, e todos parecem compartilhar o mesmo (mal) entendimento.
Se eu entendi o que Oscar está dizendo sobre a mesma classe com o mesmo hashcode, ele está propondo que apenas uma instância de uma classe com um determinado hashcode será inserida no HashMap. Por exemplo, se eu tiver uma instância de SomeClass com hashcode 1 e uma segunda instância de SomeClass com hashcode 1, apenas uma instância de SomeClass será inserida.
O exemplo de pastebin Java em http://pastebin.com/f20af40b9 parece indicar que o acima resume corretamente o que Oscar está propondo.
Independentemente de qualquer entendimento ou mal-entendido, o que acontece é que diferentes instâncias da mesma classe não são inseridas apenas uma vez no HashMap se tiverem o mesmo hashcode - não até que seja determinado se as chaves são iguais ou não. O contrato de hashcode requer que objetos iguais tenham o mesmo hashcode; no entanto, não requer que objetos desiguais tenham hashcodes diferentes (embora isso possa ser desejável por outros motivos) [1].
O exemplo pastebin.com/f20af40b9 (ao qual Oscar se refere pelo menos duas vezes) segue, mas foi ligeiramente modificado para usar asserções JUnit em vez de linhas de impressão. Este exemplo é usado para apoiar a proposta de que os mesmos hashcodes causam colisões e quando as classes são as mesmas, apenas uma entrada é criada (por exemplo, apenas uma String neste caso específico):
@Test
public void shouldOverwriteWhenEqualAndHashcodeSame() {
String s = new String("ese");
String ese = new String("ese");
// same hash right?
assertEquals(s.hashCode(), ese.hashCode());
// same class
assertEquals(s.getClass(), ese.getClass());
// AND equal
assertTrue(s.equals(ese));
Map map = new HashMap();
map.put(s, 1);
map.put(ese, 2);
SomeClass some = new SomeClass();
// still same hash right?
assertEquals(s.hashCode(), ese.hashCode());
assertEquals(s.hashCode(), some.hashCode());
map.put(some, 3);
// what would we get?
assertEquals(2, map.size());
assertEquals(2, map.get("ese"));
assertEquals(3, map.get(some));
assertTrue(s.equals(ese) && s.equals("ese"));
}
class SomeClass {
public int hashCode() {
return 100727;
}
}
No entanto, o hashcode não é a história completa. O que o exemplo pastebin negligencia é o fato de que s
e ese
são iguais: ambos são a string "ese". Assim, inserir ou obter o conteúdo do mapa usando s
ou ese
ou "ese"
como a chave são todos equivalentes porques.equals(ese) && s.equals("ese")
.
Um segundo teste demonstra que é errôneo concluir que hashcodes idênticos na mesma classe é o motivo pelo qual a chave -> valor s -> 1
é substituída por ese -> 2
quando map.put(ese, 2)
é chamada no teste um. No teste dois, s
e ese
ainda têm o mesmo hashcode (conforme verificado por assertEquals(s.hashCode(), ese.hashCode());
) E são da mesma classe. No entanto, s
e ese
são MyString
instâncias neste teste, não String
instâncias Java - com a única diferença relevante para este teste sendo os iguais: String s equals String ese
no teste um acima, enquanto MyStrings s does not equal MyString ese
no teste dois:
@Test
public void shouldInsertWhenNotEqualAndHashcodeSame() {
MyString s = new MyString("ese");
MyString ese = new MyString("ese");
// same hash right?
assertEquals(s.hashCode(), ese.hashCode());
// same class
assertEquals(s.getClass(), ese.getClass());
// BUT not equal
assertFalse(s.equals(ese));
Map map = new HashMap();
map.put(s, 1);
map.put(ese, 2);
SomeClass some = new SomeClass();
// still same hash right?
assertEquals(s.hashCode(), ese.hashCode());
assertEquals(s.hashCode(), some.hashCode());
map.put(some, 3);
// what would we get?
assertEquals(3, map.size());
assertEquals(1, map.get(s));
assertEquals(2, map.get(ese));
assertEquals(3, map.get(some));
}
/**
* NOTE: equals is not overridden so the default implementation is used
* which means objects are only equal if they're the same instance, whereas
* the actual Java String class compares the value of its contents.
*/
class MyString {
String i;
MyString(String i) {
this.i = i;
}
@Override
public int hashCode() {
return 100727;
}
}
Com base em um comentário posterior, Oscar parece inverter o que disse anteriormente e reconhece a importância dos iguais. No entanto, ainda parece que a noção de que igual é o que importa, não a "mesma classe", não está clara (grifo meu):
"Na verdade, não. A lista é criada apenas se o hash for o mesmo, mas a chave for diferente. Por exemplo, se uma String fornece o hashcode 2345 e o Integer fornece o mesmo hashcode 2345, o inteiro é inserido na lista porque String. equals (Integer) é false. Mas se você tem a mesma classe (ou pelo menos .equals retorna true), então a mesma entrada é usada. Por exemplo, new String ("um") e `new String (" one ") usados como , usará a mesma entrada. Na verdade, este é o ponto INTEIRO do HashMap em primeiro lugar! Veja você mesmo: pastebin.com/f20af40b9 - Oscar Reyes "
versus comentários anteriores que abordam explicitamente a importância de uma classe idêntica e do mesmo código hash, sem menção de iguais:
"@delfuego: Veja você mesmo: pastebin.com/f20af40b9 Então, nesta questão, a mesma classe está sendo usada (espere um minuto, a mesma classe está sendo usada certo?) O que implica que quando o mesmo hash é usado, a mesma entrada é usado e não há "lista" de entradas. - Oscar Reyes "
ou
"Na verdade, isso aumentaria o desempenho. Quanto mais colisões eq menos entradas na eq. Hashtable menos trabalho a fazer. Não é o hash (que parece bom) nem a hashtable (que funciona muito bem), aposto que é no objeto criação onde o desempenho é degradante. - Oscar Reyes "
ou
"@kdgregory: Sim, mas apenas se a colisão acontecer com classes diferentes, para a mesma classe (que é o caso) a mesma entrada é usada. - Oscar Reyes"
Mais uma vez, posso interpretar mal o que Oscar estava realmente tentando dizer. No entanto, seus comentários originais causaram confusão suficiente que parece prudente esclarecer tudo com alguns testes explícitos para que não haja dúvidas persistentes.
[1] - From Effective Java, Second Edition por Joshua Bloch:
Sempre que ele é chamado no mesmo objeto mais de uma vez durante a execução de um aplicativo, o método hashCode deve retornar consistentemente o mesmo inteiro, desde que nenhuma informação usada em comparações de igualdade no objeto seja modificada. Este inteiro não precisa permanecer consistente de uma execução de um aplicativo para outra execução do mesmo aplicativo.
Se dois objetos são iguais de acordo com o método equal s (Obj ect), chamar o método hashCode em cada um dos dois objetos deve produzir o mesmo resultado inteiro.
Não é necessário que, se dois objetos forem desiguais de acordo com o método equal s (Object), chamar o método hashCode em cada um dos dois objetos deve produzir resultados inteiros distintos. No entanto, o programador deve estar ciente de que produzir resultados inteiros distintos para objetos desiguais pode melhorar o desempenho das tabelas hash.