Java: ¿por qué las colecciones aceptan un Comparador pero no (un hipotético) Hasher y Equator?

25

Este problema es más evidente cuando tiene diferentes implementaciones de una interfaz, y para los propósitos de una colección en particular, solo le importa la vista a nivel de interfaz de los objetos. Por ejemplo, suponga que tiene una interfaz como esta:

public interface Person {
    int getId();
}

La forma habitual de implementar hashcode()e equals()implementar clases tendría un código como este en el equalsmétodo:

if (getClass() != other.getClass()) {
    return false;
}

Esto causa problemas cuando mezclas implementaciones de Personen a HashMap. Si el HashMapúnico se preocupa por la vista de nivel de interfaz de Person, entonces podría terminar con duplicados que difieren solo en sus clases de implementación.

Puede hacer que este caso funcione utilizando el mismo equals()método liberal para todas las implementaciones, pero luego corre el riesgo de equals()hacer lo incorrecto en un contexto diferente (como comparar dos correos Personelectrónicos respaldados por registros de bases de datos con números de versión).

Mi intuición me dice que la igualdad debe definirse por colección en lugar de por clase. Al usar colecciones que dependen del pedido, puede usar una costumbre Comparatorpara elegir el orden correcto en cada contexto. No existe un análogo para las colecciones basadas en hash. ¿Por qué es esto?

Solo para aclarar, esta pregunta es distinta de " ¿Por qué está .compareTo () en una interfaz mientras que .equals () está en una clase en Java? " Porque trata de la implementación de colecciones. compareTo()y equals()/ o hashcode()ambos sufren el problema de la universalidad cuando se usan colecciones: no se pueden elegir diferentes funciones de comparación para diferentes colecciones. Entonces, para los propósitos de esta pregunta, la jerarquía de herencia de un objeto no importa en absoluto; lo único que importa es si la función de comparación se define por objeto o por colección.

Sam
fuente
55
Siempre puede introducir objetos de contenedor para Personque implementen lo esperado equalsy el hashCodecomportamiento. Entonces tendrías un HashMap<PersonWrapper, V>. Este es un ejemplo en el que un enfoque de OOP puro no es elegante: no todas las operaciones en un objeto tienen sentido como método de ese objeto. Conjunto de java Objecttipo es una amalgama de diferentes responsabilidades - sólo el getClass, finalizey toStringmétodos parece remotamente justificable por las mejores prácticas de hoy en día.
amon
1
1) En C # puede pasar una IEqualityComparer<T>colección basada en hash. Si no especifica uno, utiliza una implementación predeterminada basada en Object.Equalsy Object.GetHashCode(). 2) La anulación de la OMI Equalsen un tipo de referencia mutable rara vez es una buena idea. De esa manera, la igualdad predeterminada es bastante estricta, pero puede usar una regla de igualdad más relajada cuando la necesite a través de una costumbre IEqualityComparer<T>.
CodesInChaos
2
Meta pregunta relacionada: ¿Son estas preguntas duplicadas entre sí?

Respuestas:

23

Este diseño a veces se conoce como "Igualdad universal", es la creencia de que si dos cosas son iguales o no es una propiedad universal.

Además, la igualdad es una propiedad de dos objetos, pero en OO, siempre se llama a un método en un solo objeto , y ese objeto decide únicamente cómo manejar esa llamada al método. Entonces, en un diseño como el de Java, donde la igualdad es una propiedad de uno de los dos objetos que se comparan, ni siquiera es posible garantizar algunas propiedades básicas de igualdad como la simetría ( a == bb == a), porque en el primer caso, el método se llama ay, en el segundo caso, se llama by, debido a los principios básicos de OO, es adecisión exclusiva (en el primer caso) obLa decisión de (en el segundo caso) si se considera igual o no al otro. La única forma de ganar simetría es hacer que los dos objetos cooperen, pero si no lo hacen ... mala suerte.

Una solución sería hacer que la igualdad no sea una propiedad de un objeto, sino una propiedad de dos objetos o una propiedad de un tercer objeto. Esa última opción también resuelve el problema de la igualdad universal, porque si hace de la igualdad una propiedad de un tercer objeto de "contexto", entonces puede imaginar tener diferentes EqualityComparerobjetos para diferentes contextos.

Este es el diseño elegido para Haskell, por ejemplo, con la Eqclase de tipos. También es el diseño elegido por algunas bibliotecas de Scala de terceros (ScalaZ, por ejemplo), pero no el núcleo de Scala o la biblioteca estándar, que utiliza la igualdad universal para la compatibilidad con la plataforma host subyacente.

Es, curiosamente, también el diseño elegido con las interfaces Comparable/ Java Comparator. Los diseñadores de Java claramente estaban conscientes del problema, pero por alguna razón solo lo resolvieron para ordenar, pero no para la igualdad (o hashing).

Entonces, en cuanto a la pregunta

¿Por qué hay una Comparatorinterfaz pero no Hashery Equator?

La respuesta es "No sé". Claramente, los diseñadores de Java eran conscientes del problema, como lo demuestra la existencia de Comparator, pero obviamente no lo consideraban un problema para la igualdad y el hash. Otros idiomas y bibliotecas toman diferentes decisiones.

Jörg W Mittag
fuente
77
+1, pero tenga en cuenta que hay lenguajes OO donde existe el envío múltiple (Smalltalk, Common Lisp). Por lo tanto, siempre es demasiado fuerte en la siguiente oración: "en OO, siempre se llama a un método en un solo objeto".
coredump
He encontrado la cita que estaba buscando; de acuerdo con JLS 1.0, The methods equals and hashCode are declared for the benefit of hashtables such as java.util.Hashtablees decir, ambos equalsy hashCodefueron introducidos como Objectmétodos por los desarrolladores de Java solo por el bien Hashtable: no hay una noción de UE o nada silimar en ninguna parte de la especificación, y la cita es lo suficientemente clara para mí; si no fuera por el Hashtable, equalsprobablemente habría estado en una interfaz como Comparable. Como tal, aunque antes creía que su respuesta era correcta, ahora la considero sin fundamento.
vaxquis
@ JörgWMittag fue un error tipográfico, IFTFY. Por cierto, hablando de clone: originalmente era un operador , no un método (ver la especificación del lenguaje Oak), cita: The unary operator clone is applied to an object. (...) The clone operator is normally used inside new to clone the prototype of some class, before applying the initializers (constructors)los tres operadores similares a palabras clave eran instanceof new clone(sección 8.1, operadores). Supongo que esa es la razón real (histórica) del clone/ Cloneablemess: Cloneablefue simplemente una invención posterior, y el clonecódigo existente se modificó con él.
vaxquis
2
"Este es el diseño elegido para Haskell, por ejemplo, con la clase de tipo Eq" Esto es cierto, pero vale la pena señalar que Haskell declara explícitamente por adelantado que dos objetos de diferentes tipos nunca son iguales, mientras que el enfoque de Java no lo es. La operación de igualdad es, por lo tanto, parte del tipo (por lo tanto, "typeclass") no es parte de un tercer valor de contexto.
Jack
19

La verdadera respuesta a

¿Por qué hay una Comparatorinterfaz pero no Hashery Equator?

es, cita cortesía de Josh Bloch :

Las API de Java originales se realizaron muy rápidamente en un plazo ajustado para cumplir con una ventana de mercado de cierre. El equipo original de Java hizo un trabajo increíble, pero no todas las API son perfectas.

El problema reside únicamente en la historia de Java, al igual que con otros asuntos similares, por ejemplo .clone()vs Cloneable.

tl; dr

es por razones históricas principalmente; El comportamiento / abstracción actual se introdujo en JDK 1.0 y no se solucionó más adelante porque era prácticamente imposible mantener la compatibilidad con el código hacia atrás.


Primero, resumamos un par de hechos conocidos de Java:

  1. Java, desde el principio hasta el día de hoy, era orgullosamente compatible con versiones anteriores, y requería que las API heredadas aún se admitieran en versiones más nuevas,
  2. como tal, casi todos y cada uno de los idiomas introducidos con JDK 1.0 vivieron hasta nuestros días,
  3. Hashtable, .hashCode()y .equals()se implementaron en JDK 1.0, ( Hashtable )
  4. Comparable/ Comparatorfue introducido en JDK 1.2 ( Comparable ),

Ahora, sigue:

  1. era virtualmente imposible y sin sentido adaptar .hashCode()e .equals()interfaces distintas mientras se mantenía la compatibilidad con versiones anteriores después de que las personas se dieron cuenta de que hay mejores abstracciones que ponerlas en superobjeto, porque, por ejemplo, todos y cada uno de los programadores de Java de 1.2 sabían que todos Objectlos tenían, y tenían permanecer allí físicamente para proporcionar compatibilidad con el código compilado (JVM) también, y agregar una interfaz explícita a cada Objectsubclase que realmente los implementara haría que este desastre fuera igual (sic!) a Clonableuno ( Bloch discute por qué Cloneable apesta , también discutido en, por ejemplo, EJ 2nd y muchos otros lugares, incluido SO),
  2. los dejaron allí para que la generación futura tenga una fuente constante de WTF.

Ahora, puedes preguntar "¿qué Hashtabletiene todo esto"?

La respuesta es: hashCode()/ equals()contrato y habilidades de diseño de lenguaje no tan buenas de los principales desarrolladores de Java en 1995/1996.

Cita de Java 1.0 Language Spec, con fecha de 1996 - 4.3.2 The Class Object, p.41:

Los métodos equalsy hashCodese declaran en beneficio de java.util.Hashtabletablas hash como (§21.7). El método igual define una noción de igualdad de objetos, que se basa en el valor, no en la referencia, en la comparación.

(tenga en cuenta esta declaración exacta se ha cambiado en versiones posteriores, decir, cita: The method hashCode is very useful, together with the method equals, in hashtables such as java.util.HashMap., por lo que es imposible hacer la directa Hashtable- hashCode- equalsconexión sin leer JLS históricos!)

El equipo de Java decidió que querían una buena colección de estilo de diccionario, y crearon Hashtable(buena idea hasta ahora), pero querían que el programador pudiera usarla con la menor curva de código / aprendizaje posible (¡vaya! ¡Problemas entrantes!) - y, dado que todavía no había genéricos [después de todo, es JDK 1.0], eso significaría que cada Object puesto Hashtabletendría que implementar explícitamente alguna interfaz (y las interfaces todavía estaban en su inicio en ese momento ... ¡ Comparableaún no !) , haciendo que esto sea un elemento disuasorio para usarlo para muchos, o Objecttendría que implementar implícitamente algún método de hash.

Obviamente, optaron por la solución 2, por los motivos descritos anteriormente. Sí, ahora sabemos que estaban equivocados. ... es fácil ser inteligente en retrospectiva. risita

Ahora, hashCode() requiere que cada objeto que lo tenga tenga un equals()método distinto , por lo que era bastante obvio que también equals()tenía que colocarse Object.

Desde los predeterminados implementaciones de esos métodos en válida ay b Objects son esencialmente inútiles por ser redundante (haciendo a.equals(b) igual a a==by a.hashCode() == b.hashCode() aproximadamente igual a a==btambién, a menos que hashCodey / o equalsestá anulado, o GC cientos de miles de Objects durante el ciclo de vida de la aplicación 1 ) , es seguro decir que se proporcionaron principalmente como una medida de respaldo y por conveniencia de uso. Así es exactamente como llegamos al conocido hecho de que siempre anulamos tanto .equals()&.hashCode() si tiene la intención de comparar realmente los objetos o almacenarlos en hash. Reemplazar solo uno de ellos sin el otro es una buena manera de atornillar su código (mediante resultados de comparación perversos o valores de colisión de cubo increíblemente altos), y entenderlo es una fuente de confusión y errores constantes para principiantes (busque SO para ver para ti) y molestias constantes para los más experimentados.

Además, tenga en cuenta que, aunque C # trata con iguales y hashcode de una manera un poco mejor, Eric Lippert mismo afirma que cometieron casi el mismo error con C # que Sun hizo con Java años antes del inicio de C # :

Pero, ¿por qué debería ser el caso de que cada objeto pueda ser hash para insertarse en una tabla hash? Parece una cosa extraña requerir que cada objeto pueda hacer. Creo que si estuviéramos rediseñando el sistema de tipos desde cero hoy, el hash podría hacerse de manera diferente, tal vez con una IHashableinterfaz. Pero cuando se diseñó el sistema de tipos CLR no había tipos genéricos y, por lo tanto, se necesitaba una tabla hash de propósito general para poder almacenar cualquier objeto.

1, por supuesto, Object#hashCodeaún puede colisionar, pero se necesita un poco de esfuerzo para hacerlo, consulte: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6809470 e informes de errores vinculados para obtener más detalles; /programming/1381060/hashcode-uniqueness/1381114#1381114 cubre este tema más en profundidad.

vaxquis
fuente
Sin embargo, no es solo Java. Muchos de sus contemporáneos (Ruby, Python, ...) y sus predecesores (Smalltalk, ...) y algunos de sus sucesores también tienen Universal Equality y Universal Hashability (¿es una palabra?).
Jörg W Mittag
@ JörgWMittag ver programmers.stackexchange.com/questions/283194/… - Tengo que estar en desacuerdo sobre "UE" en Java; Históricamente, la UE nunca fue una preocupación real en Objectel diseño; hashability fue.
vaxquis
@vaxquis No quiero insistir en esto, pero mi comentario anterior muestra que dos objetos accesibles simultáneamente pueden tener el mismo código hash (predeterminado).
Vuelva a instalar Mónica
1
@vaxquis OK. Yo compro eso. Mi preocupación es que alguien que esté aprendiendo vea esto y piense que es inteligente usando el hashcode del sistema en lugar de iguales, etc. no hay forma de reproducir el problema de manera confiable.
JimmyJames
1
Esta debería ser la respuesta aceptada, ya que la conclusión de la respuesta aceptada es "no sé"
Phoenix