2010-10-28 17 views
35

Tengo un rango de objetos que tienen un campo long cuyo valor identifica de manera única un objeto en particular en todo mi sistema, al igual que un GUID. He reemplazado Object.equals() para usar esta identificación para comparar, porque quiero que funcione con copias del objeto. Ahora también quiero sobrescribir Object.hashCode(), lo que básicamente significa mapear long a un valor de retorno de int.¿Cómo debo mapear long to int en hashCode()?

Si entendí correctamente el propósito de hashCode, se usa principalmente en tablas hash, por lo que sería deseable una distribución uniforme. Esto significaría que simplemente devolver id % 2^32 sería suficiente. ¿Eso es todo, o debería estar al tanto de otra cosa?

+0

btw, incluso si solo desea guardar los 32 bits más bajos, no es necesario el funcionamiento del módulo. Casting a 'int' es suficiente:' int hashCode = (int) id'. – Grodriguez

+0

@Grodriguez lo siento, pero esta respuesta es terrible! hará que muchos objetos tengan el mismo código hash que creará todo tipo de colisiones hash. Siempre quiere códigos hash distribuidos uniformemente. También la respuesta aceptada no es la mejor solución, ya que java 8 presentó una mejor solución. Consulte la respuesta dada por "Nathan", ya que 'Long.hashcode (long)' no crea un nuevo objeto en la pila –

+1

@Neuron Cualquier función hash que mapee un valor de 64 bits en uno de 32 bits causará " muchos objetos tienen el mismo hashcode ". Simplemente no hay forma de evitar eso. Además, no hay garantía de que '(this.longValue()^(this.longValue() >>> 32))' produzca códigos hash más distribuidos uniformemente que solo mantener los 32 bits más bajos del valor. – Grodriguez

Respuesta

66

Desde Java 8 se puede utilizar

Long.hashCode(guid); 

Para versiones anteriores de Java que puede utilizar el siguiente:

Long.valueOf(guid).hashCode(); 

Tenga en cuenta que esta solución crea un nuevo objeto de la pila, mientras que la primera no (aunque es probable que Java optimice la creación del objeto) ..

Mirando los documentos, en ambos sentidos simplemente use el siguiente algoritmo:

(int)(this.longValue()^(this.longValue()>>>32)) 

Estas son soluciones decentes, ya que hacen uso de la biblioteca Java, siempre es mejor aprovecharse de algo que ya se ha probado.

+2

Esto puede ser costoso ya que requiere la creación de objetos (de ahí la alternativa de la guayaba). En cuanto al algoritmo en sí, el único momento en que es peligroso es cuando los 32 bits superiores e inferiores tienen significados correlacionados. Por ejemplo, este sería un código hash horrible para una clase 'Point' que almacena una coordina xey de 32 bits en una única longitud. –

+1

Es completamente posible que la máquina virtual optimice la creación del objeto. No es que me gustaría confiar en eso. – TofuBeer

+0

@Mark realmente funcionaría bien para una clase Point también. de hecho, si tuviera una clase de punto con xey diferentes, generaría hashCode de manera similar (x^y). – james

5

Ha entendido correctamente el propósito de hashCode. Sí, es deseable una distribución uniforme (aunque no es un requisito real).

Sugeriría ((id >> 32)^id).

La expresión anterior:

  • Usos todos los bits del valor original, no se descarta ninguna información por adelantado. Por ejemplo, dependiendo de cómo esté generando los ID, los bits superiores podrían cambiar más frecuentemente (o lo contrario).
  • No introduce ningún sesgo hacia valores con más unos (ceros), como sería el caso si las dos mitades se combinaron con una operación OR (Y).
+0

+1. Esto es casi el hashCode definido para 'java.lang.Long', aunque usa' >>> 'en lugar de' >> '. Me pregunto si '(new Long (id)). HashCode();', o similar, estaría bien optimizado. –

+3

@Steve: no hay diferencia entre '>>>' y '>>' en este caso, ya que los 32 bits adicionales que se introducen durante el cambio se descartarán de todos modos. – Grodriguez

1
int result = (int)((longVal >> 32)^longVal); 

será más bien distribuido, porque módulo no volverá valor diferente si sólo bits superiores del valor de su tiempo ha cambiado.

9

que es un poco de una cosa menor si no estás usando Guava ya, pero la guayaba puede do this for you muy bien:

public int hashCode() { 
    return Longs.hashCode(id); 
} 

que le da el equivalente de Long.valueOf(id).hashCode():

return (int) (value^(value >>> 32)); 

Además, si tuviera otros valores u objetos que formaban parte del código hash, podría escribir

return Objects.hashCode(longValue, somethingElse, ...); 

El long sería autoboxed en un Long, por lo que obtendría el hashcode correcto como parte del hashcode general.

+0

No utilizaría una biblioteca completamente nueva solo para esto, pero nunca había oído hablar de Guava, parece bastante útil y vale la pena mirarlo desde un punto de vista más general. ¡Gracias! –

+1

@Hanno: Sí, ciertamente no valdría la pena solo por esta pequeña cosa. ¡Pero es una gran biblioteca con toneladas de útiles funciones! – ColinD

+0

No he estado haciendo mucho Java en los últimos años, pero Guava es increíble y ofrece muchas clases útiles para mejorar tu código. –

2

(l >> 32)^l es un buen hashcode en la mayoría de los casos; particularmente cuando el largo tiene una distribución uniforme.

Dado que fue la respuesta aceptada, publicaré esto para aclarar algunos de mis comentarios sobre cuándo NO es un buen hashcode por mucho tiempo.

El ejemplo que dio fue una clase Point como esto:

public class Point { 
    private final long coords; //x in high-bits, y in low 
    public int getX() { 
     return (int)(coords >> 32); 
    } 
    public int getY() { 
     return (int)coords; 
    } 
    public int hashCode() { 
     return (int)((coords >> 32)^(coords)); 
    } 
} 

Puede parecer artificial, pero de vez en cuando tener múltiples "campos" empaquetados en un largo.

Así que el campo coords representa 32 bits de xy 32 bits de y. ¿Por qué es esto un problema? Bueno, no es si cada uno de xey están distribuidos uniformemente sobre sus respectivos 32 bits. Pero eso es poco probable en la práctica. Lo que es más probable es que X e Y estén delimitados por un número. Digamos 1024 ya que son 2^10. Esto significa que como máximo se establecen las inferiores 10 bits de cada X e Y:

00000000 00000000 000000XX XXXXXXXX 00000000 00000000 000000YY YYYYYYYY 

hay 2^20 (1024 * 1024) combinaciones posibles. ¿Pero cuál es la operación que hashCode está haciendo?

00000000 00000000 000000XX XXXXXXXX 
^ 00000000 00000000 000000YY YYYYYYYY 
------------------------------------- 
= 00000000 00000000 000000?? ???????? 

Hay como máximo 2^10 (1024) posibles valores hashCode ya que sólo los bajos 10 bits nunca pueden ser distinto de cero. La relación de valores hash a valores reales es 1024:(1024*1024) o 1:1024. Así que desde el principio hay una probabilidad de 1/1024 de que dos números tengan el mismo hash.

Ahora calculemos la probabilidad de una colisión aplicando matemática del birthday problem. Sea p (n) la probabilidad de que con n valores habrá al menos una colisión. Sabemos que p (1025+) = 1 ya que solo hay 1024 valores.

p(n) = 1 - (n! * (1024 choose n))/1024^n 

Esto se resuelve a lo siguiente:

n: p(n) 
1: 0.00000 
2: 0.00098 
3: 0.00293 
4: 0.00585 
5: 0.00973 
6: 0.01457 
... 
38: 0.50096 
... 
79: 0.95444 
... 
148: 0.99999 

Con tan sólo 38 artículos, es probable que una colisión. Con 148 artículos, hay un 99.999% de posibilidades de (al menos una) colisión. Con 148 artículos, cada artículo tiene un 7% de probabilidad de colisionar con otro artículo. Con una función de hash adecuada, tomando conocimiento del dominio, estos números podrían bajar fácilmente a 0.

En otras palabras, conocer su dominio y cómo suceden las cosas en la práctica son la clave para hacer un hash de ejecución.Las funciones de la biblioteca intentan hacer el mejor trabajo posible sin saber nada sobre su dominio y, para ser eficaces, suelen confiar en una distribución de datos que no se realizará en la práctica.

+0

En última instancia, esta respuesta es ortogonal a mi afirmación original de que usar x^y para una clase de punto es un hash razonable. Su argumento aquí es que no es razonable + si + xey están limitados a un máximo de 1024 puntos válidos, pero no contradice mi afirmación original. – james

+0

@james: Eso es solo ser innecesariamente ignorante, sin embargo, es mi punto. ¿Con qué frecuencia en la práctica hay un conjunto de puntos distribuidos uniformemente en su dominio? Casi nunca. Hay una razón por la que Bloch sugiere este tipo de receta para hashCode: 'somePrime * getX() + getY()'. No es genial, pero lo mejor está ahí para tratar de "descorrelacionar" los datos sin saber nada sobre el dominio. Así es también como funciona la verdadera clase 'Point2D', en general. –

+0

@james: por cierto, esto es igual de relevante para xey también está limitado por 2^30, aunque para 2^30 esperarías una tonelada de colisiones de todos modos; no hay nada que puedas hacer al respecto. 1024 fue elegido simplemente porque es fácil de explicar. –

3

Java 8 agrega Long.hashCode(long) al JDK.

El siguiente código podría producir un mayor rendimiento. Este código reduce el cálculo a 32 bits int en lugar de calcular con 64-bit long. Esto puede hacer una diferencia en arquitecturas de 32 bits y más pequeñas. Los procesos de 32 bits en máquinas x86 podrían optimizar esto en una única instrucción que simplemente registra XORs 2.

return (int)(value^(value >>> 32));

Como se ha señalado en otras respuestas, esto hace no tienen una buenaavalanche effect y por lo tanto podría dar lugar a colisiones. Uno podría ir con funciones hash criptográficas para garantizar un efecto de avalancha alto. Sin embargo, hay otros algoritmos como Murmur Hash (más information) que tienen un efecto de avalancha muy bueno pero no consumen tanto tiempo de CPU.