2008-10-09 24 views
62

En pocas palabras, el contrato hashCode, según Object.hashCode de Java():¿Cómo debe una unidad probar el contrato hashCode-igual?

  1. El código hash no debería cambiar a menos que algo que afecta a los iguales() cambia
  2. iguales() implica códigos hash son = =

Asumamos interés principalmente en objetos de datos inmutables: su información nunca cambia una vez que se han construido, por lo que se supone que el valor n. ° 1 es válido. Eso deja el # 2: el problema es simplemente uno de confirmar que igual implica un código hash ==.

Obviamente, no podemos probar todos los objetos de datos concebibles a menos que ese conjunto sea trivialmente pequeño. Entonces, ¿cuál es la mejor manera de escribir una prueba unitaria que probablemente atrape los casos comunes?

Dado que las instancias de esta clase son inmutables, existen formas limitadas de construir dicho objeto; esta prueba unitaria debe cubrir todos ellos si es posible. Fuera de mi cabeza, los puntos de entrada son los constructores, la deserialización y los constructores de subclases (que deberían poder reducirse al problema de llamada del constructor).

[Voy a tratar de responder mi propia pregunta a través de la investigación. Aportaciones de otros StackOverflowers es un mecanismo de seguridad bienvenidos a este proceso.]

[Esto podría ser aplicable a otros lenguajes orientados a objetos, así que estoy añadiendo esa etiqueta.]

Respuesta

9

Mi consejo sería pensar en qué/cómo esto puede alguna vez no ser cierto, y luego escribir algunas pruebas unitarias que se dirigen a esas situaciones.

Por ejemplo, supongamos que tiene una clase personalizada Set. Dos conjuntos son iguales si contienen los mismos elementos, pero es posible que las estructuras de datos subyacentes de dos conjuntos iguales difieran si esos elementos se almacenan en un orden diferente. Por ejemplo:

MySet s1 = new MySet(new String[]{"Hello", "World"}); 
MySet s2 = new MySet(new String[]{"World", "Hello"}); 
assertEquals(s1, s2); 
assertTrue(s1.hashCode()==s2.hashCode()); 

En este caso, el orden de los elementos en los conjuntos podría afectar a su hachís, dependiendo del algoritmo de hash que haya implementado. Así que este es el tipo de prueba que escribiría, ya que prueba el caso donde sé que sería posible que un algoritmo hash produzca resultados diferentes para dos objetos que he definido para que sean iguales.

Debe usar un estándar similar con su clase personalizada, sea lo que sea.

1

Este es uno de los únicos casos en los que tendría múltiples afirmaciones en una prueba. Como necesita probar el método equals, también debe verificar el método hashCode al mismo tiempo. Por lo tanto, en cada uno de los casos de prueba del método equals, compruebe también el contrato hashCode.

A one = new A(...); 
A two = new A(...); 
assertEquals("These should be equal", one, two); 
int oneCode = one.hashCode(); 
assertEquals("HashCodes should be equal", oneCode, two.hashCode()); 
assertEquals("HashCode should not change", oneCode, one.hashCode()); 

Y, por supuesto, la comprobación de un buen hashCode es otro ejercicio. Honestamente, no me molestaría en hacer doble comprobación para asegurarme de que el hashCode no cambiara en la misma ejecución, ese tipo de problema se maneja mejor al capturarlo en una revisión de código y ayudar al desarrollador a entender por qué no es una buena manera para escribir métodos hashCode.

4

Recomendaría el EqualsTester de GSBase. Básicamente, lo que quieres.Sin embargo, tengo dos (menores) problemas:

  • El constructor hace todo el trabajo, lo que no considero una buena práctica.
  • Falla cuando una instancia de clase A es igual a una instancia de una subclase de clase A. Esto no es necesariamente una violación del contrato igual.
2

[En el momento de escribir estas líneas, se publicaron otros tres respuestas.]

Reiterar, el objetivo de mi pregunta es encontrar casos estándar de pruebas para confirmar que hashCode y equals está de acuerdo con cada una otro. Mi enfoque para esta pregunta es imaginar los caminos comunes que toman los programadores al escribir las clases en cuestión, es decir, datos inmutables. Por ejemplo:

  1. Has escrito equals() sin escribir hashCode(). Esto a menudo significa que la igualdad se definió como la igualdad de los campos de dos instancias.
  2. Escribió hashCode() sin escribir equals(). Esto puede significar que el programador buscaba un algoritmo de hash más eficiente.

En el caso del n. ° 2, el problema parece no existir para mí. No se han realizado instancias adicionales equals(), por lo que no se requieren instancias adicionales para tener códigos hash iguales. En el peor de los casos, el algoritmo hash puede producir un peor rendimiento para los mapas hash, lo cual está fuera del alcance de esta pregunta.

En el caso del n. ° 1, la prueba de unidad estándar implica la creación de dos instancias del mismo objeto con los mismos datos pasados ​​al constructor y la verificación de códigos hash iguales. ¿Qué hay de los falsos positivos? Es posible elegir los parámetros del constructor que acaban de producir los mismos códigos hash en un algoritmo sin embargo poco sólido. Una prueba unitaria que tienda a evitar dichos parámetros cumpliría con el espíritu de esta pregunta. El atajo aquí es para inspeccionar el código fuente de equals(), piénselo bien y escriba una prueba basada en eso, pero si bien esto puede ser necesario en algunos casos, también puede haber pruebas comunes que detectan problemas comunes, y dichas pruebas también cumplen con los requisitos. espíritu de esta pregunta.

Por ejemplo, si la clase a ensayar (llamarlo de datos) tiene un constructor que toma una cadena, y las instancias construido a partir de cadenas que se equals() cedido casos que eran equals(), entonces una buena prueba sería probablemente prueba:

  • new Data("foo")
  • otra new Data("foo")

podríamos incluso comprobar el código hash de new Data(new String("foo")), para forzar la cadena para no ser internado, aunque es más probable que arroje un código hash correcto que Data.equals() es dar un resultado correcto, en mi opinión.

La respuesta de Eli Courtwright es un ejemplo de una forma de romper el algoritmo hash basado en el conocimiento de la especificación equals. El ejemplo de una colección especial es bueno, ya que los Collection hechos por el usuario aparecen algunas veces, y son bastante propensos a los bloqueos en el algoritmo hash.

6

Vale la pena utilizar los complementos junit para esto. Consulte la clase EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ puede extender esto e implementar createInstance y createNotEqualInstance, esto comprobará que los métodos equals y hashCode son correctos.

60

EqualsVerifier es un proyecto de código abierto relativamente nuevo y hace un muy buen trabajo al probar el contrato igual. No tiene el issues que tiene el EqualsTester de GSBase. Sin duda lo recomendaría.

+7

Sólo _using_ EqualsVerifier me enseñó algo sobre Java! – David

+0

¿Estoy loco? ¡EqualsVerifier no pareció verificar la semántica del Value Object en absoluto! Cree que mi clase implementa correctamente equals(), pero mi clase usa el comportamiento predeterminado "==". ¡¿Cómo puede ser esto?! Debo extrañar algo simple. –

+0

Aha! Si no anulo equals(), entonces EqualsVerifier asume que todo está bien? Eso no es lo que esperaría. Esperaría el mismo comportamiento para no anular equals() como para anular equals() para hacer exactamente lo mismo que super.equals(). Extraño. –

0

Si tengo una clase Thing, como la mayoría de las demás hago, escribo una clase ThingTest, que contiene todas las pruebas unitarias para esa clase. Cada ThingTest tiene un método

public static void checkInvariants(final Thing thing) { 
    ... 
} 

y si la clase Thing anula hashCode y es igual que tiene un método

public static void checkInvariants(final Thing thing1, Thing thing2) { 
    ObjectTest.checkInvariants(thing1, thing2); 
    ... invariants that are specific to Thing 
} 

Ese método es responsable de comprobar todos invariantes que están diseñados para mantener entre cualquier par de Thing objetos. El método ObjectTest al que delega es responsable de verificar todas las invariantes que deben contenerse entre cualquier par de objetos. Como equals y hashCode son métodos de todos los objetos, ese método verifica que hashCode y equals sean consistentes.

Luego tengo algunos métodos de prueba que crean pares de objetos Thing, y los paso al método checkInvariants por parejas. Utilizo la partición de equivalencia para decidir qué pares valen la pena probar. Por lo general, creo cada par para que sea diferente en un solo atributo, más una prueba que prueba dos objetos equivalentes.

También a veces tienen un método 3 checkInvariants argumento, a pesar de que encuentre y que esté menos útil en defectos findinf, por lo que no hacen esto a menudo

+0

Esto es similar a lo que otro cartel menciona aquí: http://stackoverflow.com/a/190989/545127 – Raedwald

Cuestiones relacionadas