2010-02-05 11 views
14

Tenemos una base de código OO, donde en mucho de los casos hashcode() y equals() simplemente no funcionan, sobre todo por la siguiente razón:Java: ¿forma limpia de lanzar automáticamente UnsupportedOperationException al llamar a hashCode() y equals()?

No hay manera de extender una clase instanciable y añadir una valor componente conservando el contrato igual a , a menos que esté dispuesto a renunciar a los beneficios de la abstracción orientada a objetos .

Eso es una cita de "Effective Java" por Joshua Bloch y hay más en ese tema en un gran artículo Artima aquí:

http://www.artima.com/lejava/articles/equality.html

y estamos perfectamente bien con eso, esto es no de qué se trata esta pregunta.

La pregunta es: se ve que es un hecho que en algunos casos no se puede satisfacer el contrato equals(), lo que sería una forma limpia para hacer automáticamente hashcode() y equals() lanzar una UnsupportedOperationException?

¿Funcionaría una anotación? Estoy pensando en algo como @NotNull: cada violación de contrato @NotNull arroja una excepción automáticamente y usted no tiene nada más que hacer además de anotar sus parámetros/valor de retorno con @NotNull.

Es conveniente, porque tiene 8 caracteres ("@NotNull") en lugar de repetir constantemente el mismo código de excepción de verificación/lanzamiento.

En el caso de que me preocupa, en cada aplicación, donde hashCode()/equals() no tiene sentido, estamos repitiendo siempre lo mismo:

@Override 
public int hashCode() { 
    throw new UnsupportedOperationException("contract violation: calling hashCode() on such an object makes no sense"); 
} 

@Override 
public boolean equals(Object o) { 
    throw new UnsupportedOperationException("contract violation: calling equals() on such an object makes no sense"); 
} 

Sin embargo, esto es propenso a errores: es posible que por error se olvide cortar/pegar esto y puede ocasionar que los usuarios hagan un uso indebido de dichos objetos (digamos al tratar de ponerlos en las colecciones predeterminadas de Java).

O si la anotación no se puede hacer para crear este comportamiento, ¿funcionaría el AOP?

Curiosamente, el verdadero problema es la presencia de hashCode() y equals() en la parte superior de la jerarquía de Java que simplemente no tiene sentido en algunos casos. Pero entonces, ¿cómo lidiamos con este problema limpiamente?

+4

+1 por negarse a implementar hashCode e igualar cuando no sea necesario, e incluso asegurarse de que no se puedan invocar emitiendo una excepción. Este es un cambio bienvenido al mantra que a menudo escuchas: lo primero que debes hacer es implementar esos dos métodos (y pensar mucho en ellos para que funcionen correctamente), incluso cuando la mayoría de los objetos nunca necesitan ninguno de los dos métodos. – Thilo

+3

Un problema relacionado que tengo con los métodos autogenerados que Eclipse le brinda cuando escribe una nueva clase que implementa una interfaz es que todos se generan para 'return null',' return false', 'do nothing'. Me gustaría que el valor predeterminado sea 'throw UnsupportedOperationException (" TODO ")'. – Thilo

+3

@Thilo Hago exactamente eso con mis plantillas de Eclipse, todos los cuerpos de métodos generados lanzan 'UnsupportedOperationException' –

Respuesta

4

¿Por qué no dejas que tu IDE (Eclipse/NetBeans/IntelliJ) genere los métodos hashCode() y equals() para ti? Están haciendo un buen trabajo en eso.

AOP funcionará, por supuesto, pero es una gran complicación. Y esto significará que no podrá usar estos objetos casi sin recolección ni utilidad.

La otra solución lógica es simplemente eliminar las implementaciones de esos métodos donde no funcionan, dejando efectivamente solo las implementaciones en Object.

+0

@Bozho: +1, eso es lo que Hemal Panda dijo que estaba haciendo en su comentario y yo no lo pensé (incluso es difícil que usemos plantillas personalizadas). Estaba demasiado concentrado en tener algo como "@NotNull" :) – SyntaxT3rr0r

10

Estoy de acuerdo con su evaluación de que esto es un problema con hashCode y equals que se define en Object en primer lugar. Desde hace tiempo sostengo que la igualdad debe manejarse del mismo modo que ordenar, con una interfaz que dice "puedo compararme con una instancia de X" y otra que dice "puedo comparar dos instancias de X".

Por otro lado, ¿este en realidad ha causado algún error? ¿Han intentado las personas utilizar equals y hashCode donde no deberían?Porque incluso si puede hacer que cada clase en su base de código arroje una excepción cuando esos métodos se llaman de manera inapropiada, eso no será cierto para otras clases que esté utilizando, ya sea desde el JDK o desde bibliotecas de terceros.

Estoy seguro de que podría hacer esto con AOP de una forma u otra, ya sea que se trate de procesamiento de anotación normal o algo más, pero ¿tiene evidencia de que la recompensa valdría la pena?

Otra forma de ver las cosas: esto es solamente en el caso en que está extendiendo otra clase que ya se anula hashCode y equals, ¿verdad? De lo contrario, puede usar la naturaleza "igualdad = identidad" de los métodos hashCode/equals de Object, que aún pueden ser útiles. ¿Tienes muchas clases que entran en esta categoría? ¿No podría simplemente escribir una prueba unitaria para encontrar todos esos tipos mediante reflexión, y verificar que esos tipos arrojen una excepción cuando llame al hashCode/equals? (Esto podría ser automático si tienen un constructor sin parámetros, o tener una lista manual de los tipos que se han verificado; la prueba de la unidad podría fallar si hay un nuevo tipo que no está en la lista de "bienes conocidos")

+0

De acuerdo con el enfoque de AOP. –

+6

+1 para querer una interfaz separada para equals/hashCode. Eso evitaría que las personas usen objetos como claves hash que simplemente no funcionan allí (una interfaz inmutable también sería bueno en ese sentido). – Thilo

+0

Hola Jon, con respecto a la recompensa/esfuerzo esto es por supuesto una preocupación: pero probablemente se podría haber dicho lo mismo si, cuando salió la anotación, sugerí una anotación @NotNull. Todo lo que tomaría sería que alguien escribiera esto una vez, luego todos podríamos reutilizarlo. Curiosamente estoy usando plantillas personalizadas y no pensé simplemente en hacer, de forma predeterminada, hashCode() y equals() lanzar UnsupportedOperationException y luego simplemente cambiar la implementación para las clases donde equals() y hashCode() tiene sentido. – SyntaxT3rr0r

6

No veo por qué piensas que "en algún caso no puedes satisfacer el contrato igual()"? El significado de igualdad está definido por la clase. Por lo tanto, usar el igual de Object es perfectamente válido. Si no está reemplazando a los iguales, entonces está definiendo cada instancia como única.

Parece haber una idea errónea de que igual es uno de esos métodos que siempre debe anularse, y que debe verificar todos sus campos. Yo argumentaría lo contrario: no anule a los iguales a menos que su definición de igualdad sea diferente.

También estoy en desacuerdo con el artículo artima, en particular "Pitfall # 3: Definición de iguales en términos de campos mutables". Es perfectamente válido para una clase definir su igualdad basada en campos mutables. Depende del usuario ser consciente de esto cuando lo usa con colecciones. Si un objeto mutable define su igualdad en su estado mutable, entonces no espere que dos instancias sean iguales después de que una haya cambiado.

Creo que tirar UnsupportedOperation infringe el sprint de iguales. Objeto de igual estados:

El método equals de la clase Object implementa el posible relación de equivalencia más exigente en objetos; es decir, para cualquier valor de referencia no nulo xey, este método devuelve verdadero si y solo si xy se refieren al mismo objeto (x == y tiene el valor verdadero).

Por lo tanto, debería ser capaz de llamar a iguales y obtener un valor verdadero o falso dependiendo de la definición de igualdad de los Objetos o la definición igual a anulada.

+1

+1: incluso si nada más es útil, aún podrías aplicar la identidad del objeto como la medida de igualdad simplemente usando la implementación 'equals' /' hashCode' en 'Object '. –

+0

@Joachim: Sí, pero si la igualdad de objeto no es significativa para su clase, y no necesita igual/hashCode en absoluto, lanzar UnsupportedOperation tiene más sentido. Por lo menos, esto ayuda a encontrar errores cuando alguna parte del código llama erróneamente a estos métodos. – Thilo

+0

@Kuo: -1, no lo creo, lo sé, y ha sido probado :) No estás de acuerdo con Martin Odersky (el autor de Scala) y Joshua Bloch. Multa. Tienes derecho a tus opiniones. Se ha demostrado que simplemente no puede garantizar el contrato de transitividad de iguales(), para empezar. Incluso el todopoderoso Jon Skeet de SO fame y c.l.j.p. la fama concuerda con mi afirmación de que es la presencia misma de equals() y hashCode() en la parte superior de la jerarquía de objetos que es un problema aquí. Mi preocupación es exactamente lo que comentó Thilo: donde no tiene sentido, debería lanzar UnsupportedOperationException. – SyntaxT3rr0r

1

Hay al menos dos relaciones de equivalencia que se pueden definir entre todos objetos en Java o .NET:

  • Dos referencias a objetos X e Y son totalmente equivalentes si sobrescribir X con una referencia a Y no alteraría el comportamiento actual o futuro de ningún miembro de X o Y.

  • Dos referencias de objeto X e Y tienen un estado equivalente si, en un programa que no ha persistido, los valores devueltos de la función hash relacionada con la identidad, intercambio todas las referencias a X con todas las referencias t o Y dejaría el estado del programa sin cambios.

I tienen uno de referencia (X) que apunta a una FordBlazer. Tengo otro (Y) que apunta a un SiameseCat. ¿Son equivalentes? No, no lo son, por lo que X.equals(Y) debe ser falso. El hecho de que los tipos de los objetos no tengan ninguna relación entre ellos no es un problema; en todo caso, facilita las cosas (si lo único que puede ser equivalente a un SiameseCat es otro SiameseCat, el hecho de que Y no es un SiameseCat significa que X.equals() no tiene que examinar cualquier otra cosa.

Aunque puede haber cierto debate sobre si un objeto particular debe poner en práctica la primera o la segunda definición de la equivalencia, vale la pena señalar que cualquier objeto que define equals informe distinto los objetos como desiguales independientemente de cualquier otro aspecto de su estado serán consistentes consigo mismos (si X.Equals (X) no coincide con X.Equals (Y), eso significa que Y no se comporta igual que X). , si uno no tiene nada mejor que hacer con equals, el d predeterminado La efinición heredada de object es perfectamente válida y significativa.

La única situación en la que hashCode podría tener problemas será si el código puede (erróneamente) mutar algún aspecto de un objeto mientras está almacenado en una HashTable. El remedio adecuado para eso es tener hashCode que no dependa de ningún aspecto mutable del estado de un objeto. Si el estado de un objeto no tiene aspectos inmutables significativos que no sean su clase, simplemente invente un número arbitrario para esa clase y tenga hashCode siempre devuelva eso. Las tablas hash grandes no funcionarán bien con tales objetos, pero los pequeños códigos hash funcionarán bien. El hecho de que no se pueda definir un buen código hash para un tipo no debe impedir que se use en un HashTable con una docena de elementos.

Cuestiones relacionadas