Consistencia de hashCode () en una cadena Java

134

El valor de hashCode de una cadena Java se calcula como ( String.hashCode () ):

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

¿Hay alguna circunstancia (por ejemplo, versión de JVM, proveedor, etc.) en la que la siguiente expresión se evaluará como falsa?

boolean expression = "This is a Java string".hashCode() == 586653468

Actualización n. ° 1: Si afirma que la respuesta es "sí, existen tales circunstancias", entonces dé un ejemplo concreto de cuándo "Esto es una cadena de Java" .hashCode ()! = 586653468. Intente ser tan específico / concreto como sea posible.

Actualización n. ° 2: Todos sabemos que confiar en los detalles de implementación de hashCode () es malo en general. Sin embargo, estoy hablando específicamente sobre String.hashCode (), así que mantenga la respuesta enfocada en String.hashCode (). Object.hashCode () es totalmente irrelevante en el contexto de esta pregunta.

knorv
fuente
2
¿Realmente necesitas esta funcionalidad? ¿Por qué necesitas el valor preciso?
Brian Agnew
26
@Brian: Estoy tratando de entender el contrato de String.hashCode ().
knorv
3
@Knorv No es necesario comprender exactamente cómo funciona; es más importante comprender el contrato y su significado ulterior.
mP.
45
@mP: Gracias por su aporte, pero creo que depende de mí decidir.
knorv
¿Por qué le dieron al primer personaje el mayor poder? cuando desee optimizarlo para la velocidad con el fin de preservar los cálculos adicionales, almacenará el poder del anterior, sin embargo, el anterior sería desde el último personaje hasta el primero. Esto significa que también habría errores de caché. ¿No es más eficiente tener un algoritmo de: s [0] + s [1] * 31 + s [2] * 31 ^ 2 + ... + s [n-1] * 31 ^ [n-1 ]?
desarrollador de Android

Respuestas:

101

Puedo ver esa documentación desde Java 1.2.

Si bien es cierto que en general no debe confiar en que la implementación de un código hash siga siendo la misma, ahora es un comportamiento documentado, por java.lang.Stringlo que cambiarlo contaría como romper los contratos existentes.

Siempre que sea posible, no debe confiar en que los códigos hash permanezcan igual en todas las versiones, etc., pero en mi opinión java.lang.Stringes un caso especial simplemente porque se ha especificado el algoritmo ... siempre y cuando esté dispuesto a abandonar la compatibilidad con las versiones anteriores Se especificó el algoritmo, por supuesto.

Jon Skeet
fuente
77
El comportamiento documentado de String se ha especificado desde Java 1.2 En v1.1 de la API, el cálculo del código hash no se especifica para la clase String.
Martin OConnor
En este caso, es mejor que escribamos nuestros propios códigos hash 'ight matey?
Felype
@Felype: Realmente no sé lo que estás tratando de decir aquí, me temo.
Jon Skeet
@JonSkeet Quiero decir, en este caso quizás podamos escribir nuestro propio código para generar nuestro propio hash, para otorgar portabilidad. ¿Lo es?
Felype
@Felype: no está del todo claro de qué tipo de portabilidad está hablando, ni de qué quiere decir "en este caso", ¿en qué escenario específico? Sospecho que deberías hacer una nueva pregunta.
Jon Skeet
18

Encontré algo sobre JDK 1.0 y 1.1 y> = 1.2:

En JDK 1.0.xy 1.1.x, la función hashCode para cadenas largas funcionaba muestreando cada enésimo carácter. Esto garantiza que tendrías muchas cadenas de hashing al mismo valor, lo que ralentizaría la búsqueda de Hashtable. En JDK 1.2, la función se ha mejorado para multiplicar el resultado hasta ahora por 31 y luego agregar el siguiente carácter en secuencia. Esto es un poco más lento, pero es mucho mejor para evitar colisiones. Fuente: http://mindprod.com/jgloss/hashcode.html

Algo diferente, porque parece que necesitas un número: ¿qué tal si usas CRC32 o MD5 en lugar de hashcode y estás listo para comenzar? Sin discusiones ni preocupaciones en absoluto ...

ReneS
fuente
8

No debe confiar en que un código hash sea igual a un valor específico. Solo que devolverá resultados consistentes dentro de la misma ejecución. Los documentos API dicen lo siguiente:

El contrato general de hashCode es:

  • Cada vez que se invoca en el mismo objeto más de una vez durante la ejecución de una aplicación Java, el método hashCode debe devolver consistentemente el mismo número entero, siempre que no se modifique la información utilizada en comparaciones iguales sobre el objeto. Este número entero no necesita permanecer consistente de una ejecución de una aplicación a otra ejecución de la misma aplicación.

EDITAR Dado que el javadoc para String.hashCode () especifica cómo se calcula el código hash de String, cualquier violación de esto violaría la especificación de API pública.

Martin OConnor
fuente
1
Su respuesta es válida, pero no aborda la pregunta específica formulada.
knorv
66
Ese es el contrato de código hash general , pero el contrato específico para String brinda detalles del algoritmo y anula efectivamente este contrato general IMO.
Jon Skeet el
4

Como se dijo anteriormente, en general no debe confiar en que el código hash de una clase permanezca igual. Tenga en cuenta que incluso las ejecuciones posteriores de la misma aplicación en la misma VM pueden producir valores hash diferentes. La función hash de AFAIK the Sun JVM calcula el mismo hash en cada ejecución, pero eso no está garantizado.

Tenga en cuenta que esto no es teórico. La función hash para java.lang.String se cambió en JDK1.2 (el hash anterior tenía problemas con cadenas jerárquicas como URL o nombres de archivo, ya que tendía a producir el mismo hash para cadenas que solo diferían al final).

java.lang.String es un caso especial, ya que el algoritmo de su hashCode () está (ahora) documentado, por lo que probablemente pueda confiar en eso. Todavía lo consideraría una mala práctica. Si necesita un algoritmo hash con propiedades especiales y documentadas, simplemente escriba uno :-).

sleske
fuente
44
Pero, ¿se especificó el algoritmo en los documentos anteriores a JDK 1.2? Si no, es una situación diferente. El algoritmo ahora se establece en los documentos, por lo que cambiarlo sería un cambio importante en un contrato público.
Jon Skeet
(Lo recuerdo como 1.1.) El algoritmo original (más pobre) fue documentado. Incorrectamente. El algoritmo documentado en realidad arrojó una ArrayIndexOutOfBoundsException.
Tom Hawtin - tackline
@ Jon Skeet: Ah, no sabía que el algoritmo de String.hashCode () está documentado. Por supuesto que eso cambia las cosas. Actualizado mi comentario
sleske
3

Otra cuestión (!) De la que preocuparse es el posible cambio de implementación entre versiones tempranas / tardías de Java. No creo que los detalles de implementación estén establecidos, por lo que potencialmente una actualización a una futura versión de Java podría causar problemas.

La conclusión es que no confiaría en la implementación de hashCode().

Quizás pueda resaltar qué problema realmente está tratando de resolver utilizando este mecanismo, y eso resaltará un enfoque más adecuado.

Brian Agnew
fuente
1
Gracias por tu respuesta. ¿Puede dar ejemplos concretos de cuándo "Esta es una cadena de Java" .hashCode ()! = 586653468?
knorv
1
No lo siento. Mi punto es que todo lo que pruebes puede funcionar de la manera que quieras. Pero eso todavía no es garantía. Entonces, si está trabajando en un (por ejemplo) proyecto a corto plazo en el que tiene control de la VM, etc., entonces lo anterior puede funcionar para usted. Pero no puede confiar en él en el mundo en general.
Brian Agnew
2
"una actualización a una futura versión de Java podría causar problemas". Una actualización a una futura versión de Java podría eliminar por completo el método hashCode. O haga que siempre devuelva 0 para cadenas. Eso son cambios incompatibles para ti. La pregunta es si Sun ^ HOracle ^ HTel JCP lo consideraría un cambio innovador y, por lo tanto, vale la pena evitarlo. Dado que el algoritmo está en el contrato, uno espera que lo hagan.
Steve Jessop el
@SteveJessop bien, dado que las switchdeclaraciones sobre cadenas se compilan en código que se basa en un código hash fijo particular, los cambios en Stringel algoritmo de código hash definitivamente romperían el código existente ...
Holger
3

Solo para responder a su pregunta y no para continuar ninguna discusión. La implementación de Apache Harmony JDK parece usar un algoritmo diferente, al menos se ve totalmente diferente:

Sun JDK

public int hashCode() {
    int h = hash;
    if (h == 0) {
        int off = offset;
        char val[] = value;
        int len = count;

        for (int i = 0; i < len; i++) {
            h = 31*h + val[off++];
        }
        hash = h;
    }
    return h;
}

Armonía Apache

public int hashCode() {
    if (hashCode == 0) {
        int hash = 0, multiplier = 1;
        for (int i = offset + count - 1; i >= offset; i--) {
            hash += value[i] * multiplier;
            int shifted = multiplier << 5;
            multiplier = shifted - multiplier;
        }
        hashCode = hash;
    }
    return hashCode;
}

No dudes en comprobarlo tú mismo ...

ReneS
fuente
23
Creo que solo están siendo geniales y optimizándolo. :) "(multiplicador << 5) - multiplicador" es sólo 31 * multiplicador, después de todo ...
desenrollado
Ok, era demasiado vago para comprobar eso. ¡Gracias!
ReneS
1
Pero para que quede claro desde mi lado ... Nunca confíes en el hashcode porque el hashcode es algo interno.
ReneS
1
¿Qué significan las variables de "offset", "count" y "hashCode"? supongo que "hashcode" se usa como un valor en caché, para evitar futuros cálculos, y que "count" es el número de caracteres, pero ¿cuál es el "offset"? supongamos que deseo usar este código para que sea coherente, dada una cadena, ¿qué debo hacer?
Desarrollador de Android
1
@androiddeveloper Ahora ESA es una pregunta interesante, aunque debería haberlo adivinado en función de su nombre de usuario. Según los documentos de Android , parece que el contrato es el mismo: a s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]menos que me equivoque, esto se debe a que Android usa la implementación de Sun del objeto String sin cambios.
Kartik Chugh
2

Si le preocupan los cambios y posiblemente las máquinas virtuales incompatibles, simplemente copie la implementación de código hash existente en su propia clase de utilidad y úsela para generar sus códigos hash.

Sam Barnum
fuente
Iba a decir esto. Mientras que las otras respuestas responden la pregunta, escribir una función hashCode separada es probablemente la solución adecuada para el problema de knorv.
Nick
1

El código hash se calculará en función de los valores ASCII de los caracteres en la cadena.

Esta es la implementación en la clase de cadena es la siguiente

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        hash = h = isLatin1() ? StringLatin1.hashCode(value)
                              : StringUTF16.hashCode(value);
    }
    return h;
}

Las colisiones en el código hash son inevitables. Por ejemplo, las cadenas "Ea" y "FB" dan el mismo código hash que 2236

Lourdes
fuente