¿Cuáles son las razones por las cuales Map.get (clave de objeto) no es (completamente) genérico

405

¿Cuáles son las razones detrás de la decisión de no tener un método get totalmente genérico en la interfaz de java.util.Map<K, V>.

Para aclarar la pregunta, la firma del método es

V get(Object key)

en vez de

V get(K key)

y me pregunto por qué (lo mismo para remove, containsKey, containsValue)

WMR
fuente
3
Pregunta similar con respecto a la Colección: stackoverflow.com/questions/104799/…
AlikElzin-kilaka
1
Asombroso. Estoy usando Java desde hace más de 20 años, y hoy me doy cuenta de este problema.
GhostCat

Respuestas:

260

Como lo mencionaron otros, la razón por la cual get(), etc., no es genérica porque la clave de la entrada que está recuperando no tiene que ser del mismo tipo que el objeto al que pasa get(); La especificación del método solo requiere que sean iguales. Esto se deduce de cómo el equals()método toma un objeto como parámetro, no solo el mismo tipo que el objeto.

Aunque puede ser cierto que muchas clases se han equals()definido de modo que sus objetos solo pueden ser iguales a los objetos de su propia clase, hay muchos lugares en Java donde este no es el caso. Por ejemplo, la especificación de List.equals()dice que dos objetos List son iguales si ambas son Listas y tienen el mismo contenido, incluso si son implementaciones diferentes de List. Volviendo al ejemplo en esta pregunta, de acuerdo con la especificación del método, es posible tener un Map<ArrayList, Something>y para que llame get()con un LinkedListargumento como, y debería recuperar la clave, que es una lista con el mismo contenido. Esto no sería posible si get()fuera genérico y restringiera su tipo de argumento.

newacct
fuente
28
Entonces, ¿por qué está V Get(K k)en C #?
134
La pregunta es, si desea llamar m.get(linkedList), ¿por qué no definió mel tipo como Map<List,Something>? No puedo pensar en un caso de uso donde llamar m.get(HappensToBeEqual)sin cambiar el Maptipo para obtener una interfaz tiene sentido.
Elazar Leibovich
58
Wow, grave defecto de diseño No obtienes ninguna advertencia del compilador tampoco, jodido. Estoy de acuerdo con Elazar. Si esto es realmente útil, lo cual dudo que suceda a menudo, un getByEquals (clave de objeto) suena más razonable ...
mmm
37
Parece que esta decisión se tomó sobre la base de la pureza teórica en lugar de la practicidad. Para la mayoría de los usos, los desarrolladores prefieren ver el argumento limitado por el tipo de plantilla, que tenerlo ilimitado para admitir casos extremos como el mencionado por newacct en su respuesta. Dejar las firmas sin plantilla crea más problemas de los que resuelve.
Sam Goldberg
14
@newacct: "escribir con total seguridad" es un fuerte reclamo para una construcción que puede fallar de manera impredecible en tiempo de ejecución. No limite su vista a los mapas hash que funcionan con eso. TreeMappuede fallar cuando pasa objetos del tipo incorrecto al getmétodo, pero puede pasar ocasionalmente, por ejemplo, cuando el mapa está vacío. Y lo que es peor, en el caso de que se proporcione Comparatorel comparemétodo (¡que tiene una firma genérica!) Podría llamarse con argumentos del tipo incorrecto sin ninguna advertencia no verificada. Este es un comportamiento roto.
Holger
105

Un increíble codificador de Java en Google, Kevin Bourrillion, escribió exactamente sobre este problema en una publicación de blog hace un tiempo (ciertamente en el contexto de en Setlugar de Map). La oración más relevante:

De manera uniforme, los métodos del Marco de colecciones de Java (y también la Biblioteca de colecciones de Google) nunca restringen los tipos de sus parámetros, excepto cuando es necesario para evitar que la colección se rompa.

No estoy completamente seguro de estar de acuerdo con él como principio (.NET parece estar bien, por ejemplo, requiere el tipo de clave correcto), pero vale la pena seguir el razonamiento en la publicación del blog. (Habiendo mencionado .NET, vale la pena explicar que parte de la razón por la cual no es un problema en .NET es que hay un problema mayor en .NET de variación más limitada ...)

Jon Skeet
fuente
44
Apocalisp: eso no es cierto, la situación sigue siendo la misma.
Kevin Bourrillion el
99
@ user102008 No, la publicación no está mal. Aunque an Integery a Doublenunca pueden ser iguales entre sí, sigue siendo una pregunta justa si a Set<? extends Number>contiene el valor new Integer(5).
Kevin Bourrillion
33
Nunca he querido verificar la membresía en a Set<? extends Foo>. Con mucha frecuencia cambié el tipo de clave de un mapa y luego me sentí frustrado porque el compilador no pudo encontrar todos los lugares donde el código necesitaba actualizarse. Realmente no estoy convencido de que esta sea la compensación correcta.
Porculus
44
@EarthEngine: Siempre se ha roto. Ese es el punto: el código está roto, pero el compilador no puede detectarlo.
Jon Skeet
1
Y todavía está roto, y solo nos causó un error ... respuesta increíble.
GhostCat
28

El contrato se expresa así:

Más formalmente, si este mapa contiene una asignación de una clave k a un valor v tal que (clave == nulo? K == nulo: clave.equals (k) ), entonces este método devuelve v; de lo contrario, devuelve nulo. (Puede haber como máximo una de estas asignaciones).

(mi énfasis)

y como tal, una búsqueda exitosa de claves depende de la implementación de la clave de entrada del método de igualdad. Eso no depende necesariamente de la clase de k.

Brian Agnew
fuente
44
También depende de hashCode(). Sin una implementación adecuada de hashCode (), una implementación bien implementada equals()es bastante inútil en este caso.
Rudolfson
55
Supongo que, en principio, esto le permitiría usar un proxy ligero para una clave, si la recreación de la clave completa no es práctica, siempre que equals () y hashCode () se implementen correctamente.
Bill Michell
55
@rudolfson: Hasta donde yo sé, solo un HashMap depende del código hash para encontrar el depósito correcto. Un TreeMap, por ejemplo, usa un árbol de búsqueda binario y no le importa hashCode ().
Rob
44
Estrictamente hablando, get()no necesita tomar un argumento de tipo Objectpara satisfacer el contacto. Imagine que el método get se restringiera al tipo de clave K: el contrato aún sería válido. Por supuesto, los usos donde el tipo de tiempo de compilación no era una subclase de Kahora fallarían al compilar, pero eso no invalida el contrato, ya que los contratos discuten implícitamente qué sucede si el código se compila.
BeeOnRope
16

Es una aplicación de la Ley de Postel, "sé conservador en lo que haces, sé liberal en lo que aceptas de los demás".

Las comprobaciones de igualdad se pueden realizar independientemente del tipo; El equalsmétodo se define en la Objectclase y acepta cualquiera Objectcomo parámetro. Por lo tanto, tiene sentido que la equivalencia clave y las operaciones basadas en la equivalencia clave acepten cualquier Objecttipo.

Cuando un mapa devuelve valores clave, conserva tanta información de tipo como sea posible, utilizando el parámetro de tipo.

erickson
fuente
44
Entonces, ¿por qué está V Get(K k)en C #?
1
Está V Get(K k)en C # porque también tiene sentido. La diferencia entre los enfoques Java y .NET es realmente solo quién bloquea las cosas que no coinciden. En C # es el compilador, en Java es la colección. Rabia que sobre las clases de colección inconsistentes de .NET de vez en cuando, pero Get()y Remove()aceptando sólo un tipo de juego sin duda le impide pasar accidentalmente a un valor incorrecto en.
Wormbo
26
Es una mala aplicación de la Ley de Postel. Sé liberal en lo que aceptas de los demás, pero no demasiado liberal. Esta API idiota significa que no puede distinguir entre "no en la colección" y "cometió un error de escritura estática". Muchos miles de horas de programador perdidas podrían haberse evitado con get: K -> boolean.
Juez Mental
1
Por supuesto que debería haber sido contains : K -> boolean.
Juez Mental
13

Creo que esta sección del Tutorial de genéricos explica la situación (mi énfasis):

"Debe asegurarse de que la API genérica no sea excesivamente restrictiva; debe continuar admitiendo el contrato original de la API. Considere nuevamente algunos ejemplos de java.util.Collection. La API pregenérica se ve así:

interface Collection { 
  public boolean containsAll(Collection c);
  ...
}

Un ingenuo intento de generarlo es:

interface Collection<E> { 
  public boolean containsAll(Collection<E> c);
  ...
}

Si bien esto es seguro, no cumple con el contrato original de la API. El método contiene todo () funciona con cualquier tipo de colección entrante. Solo tendrá éxito si la colección entrante realmente contiene solo instancias de E, pero:

  • El tipo estático de la colección entrante puede diferir, tal vez porque la persona que llama no sabe el tipo exacto de la colección que se está pasando, o tal vez porque es una Colección <S>, donde S es un subtipo de E.
  • Es perfectamente legítimo llamar a usesAll () con una colección de un tipo diferente. La rutina debería funcionar, devolviendo falso ".
Yardena
fuente
2
¿por qué no containsAll( Collection< ? extends E > c )entonces?
Juez Mental
1
@JudgeMental, aunque no da como ejemplo anteriormente, también es necesario para permitir containsAllcon un Collection<S>donde Ses un supertipo de E. Esto no estaría permitido si lo fuera containsAll( Collection< ? extends E > c ). Además, como se indica explícitamente en el ejemplo, es legítimo pasar una colección de un tipo diferente (con el valor de retorno entonces false).
davmac
No debería ser necesario permitir contiene todo con una colección de un supertipo de E. Sostengo que es necesario rechazar esa llamada con una comprobación de tipo estático para evitar un error. Es un contrato tonto, que creo que es el punto de la pregunta original.
Juez Mental
6

La razón es que la contención está determinada por equalsy hashCodecuáles son los métodos Objecty ambos toman un Objectparámetro. Este fue un error de diseño temprano en las bibliotecas estándar de Java. Junto con las limitaciones en el sistema de tipos de Java, obliga a todo lo que se basa en equals y hashCode Object.

La única manera de tener tablas hash con seguridad de tipos y la igualdad en Java es a evitar Object.equalsy Object.hashCodey utilizar un sustituto genérico. Java funcional viene con clases de tipo solo para este propósito: Hash<A>y Equal<A>. Se HashMap<K, V>proporciona un contenedor para que toma Hash<K>y Equal<K>en su constructor. Por lo tanto, esta clase gety containsmétodos toman un argumento genérico de tipo K.

Ejemplo:

HashMap<String, Integer> h =
  new HashMap<String, Integer>(Equal.stringEqual, Hash.stringHash);

h.add("one", 1);

h.get("one"); // All good

h.get(Integer.valueOf(1)); // Compiler error
Apocalipsis
fuente
44
Esto en sí mismo no evita que el tipo de 'get' se declare como "V get (K key)", porque 'Object' siempre es un ancestro de K, por lo que "key.hashCode ()" seguiría siendo válido.
finnw
1
Si bien no lo impide, creo que lo explica. Si cambiaron el método equals para forzar la igualdad de clase, ciertamente no podrían decirle a las personas que el mecanismo subyacente para ubicar el objeto en el mapa utiliza equals () y hashmap () cuando los prototipos de métodos para esos métodos no son compatibles.
cgp
5

Compatibilidad.

Antes de que los genéricos estuvieran disponibles, solo había get (Object o).

Si hubieran cambiado este método para obtener (<K> o), habría forzado potencialmente el mantenimiento masivo de código a los usuarios de Java solo para hacer que el código de trabajo vuelva a compilarse.

Ellos podrían haber introducido un adicional método, digamos get_checked (<K> O) y despreciar el viejo método get () así que había un camino de transición más suave. Pero por alguna razón, esto no se hizo. (La situación en la que nos encontramos ahora es que necesita instalar herramientas como findBugs para verificar la compatibilidad de tipos entre el argumento get () y el tipo de clave declarada <K> del mapa).

Los argumentos relacionados con la semántica de .equals () son falsos, creo. (Técnicamente son correctos, pero sigo pensando que son falsos. Ningún diseñador en su sano juicio hará que o1.equals (o2) sea verdadero si o1 y o2 no tienen ninguna superclase común).

Erwin Smout
fuente
4

Hay una razón más importante, no se puede hacer técnicamente, porque rompe el Mapa.

Java tiene una construcción genérica polimórfica como <? extends SomeClass>. Marcado tal referencia puede apuntar a tipo firmado con <AnySubclassOfSomeClass>. Pero el genérico polimórfico hace que esa referencia sea de solo lectura . El compilador le permite usar tipos genéricos solo como tipo de método de retorno (como getters simples), pero bloquea el uso de métodos donde el tipo genérico es argumento (como setters ordinarios). Significa que si escribe Map<? extends KeyType, ValueType>, el compilador no le permite llamar al método get(<? extends KeyType>), y el mapa será inútil. La única solución es hacer que este método no genérico: get(Object).

Owheee
fuente
¿Por qué el método set está fuertemente tipado entonces?
Sentenza
si quiere decir 'put': El método put () cambia el mapa y no estará disponible con genéricos como <? extiende SomeClass>. Si lo llamas tienes una excepción de compilación. Dicho mapa será "de solo lectura"
Owheee
1

Compatibilidad con versiones anteriores, supongo. Map(o HashMap) todavía necesita apoyo get(Object).

Anton Gogolev
fuente
13
Pero podría hacerse el mismo argumento para put(que restringe los tipos genéricos). Obtiene compatibilidad con versiones anteriores mediante el uso de tipos sin formato. Los genéricos son "opt-in".
Thilo
Personalmente, creo que la razón más probable para esta decisión de diseño es la compatibilidad con versiones anteriores.
geekdenz
1

Estaba mirando esto y pensando por qué lo hicieron de esta manera. No creo que ninguna de las respuestas existentes explique por qué no podían hacer que la nueva interfaz genérica aceptara solo el tipo adecuado para la clave. La razón real es que, aunque introdujeron genéricos, NO crearon una nueva interfaz. La interfaz del mapa es el mismo mapa antiguo no genérico, solo sirve como versión genérica y no genérica. De esta manera, si tiene un método que acepta un mapa no genérico, puede pasarlo Map<String, Customer>y aún funcionaría. Al mismo tiempo, el contrato para obtener acepta Object, por lo que la nueva interfaz también debe ser compatible con este contrato.

En mi opinión, deberían haber agregado una nueva interfaz e implementado tanto en la colección existente, pero decidieron a favor de interfaces compatibles, incluso si eso significa un peor diseño para el método get. Tenga en cuenta que las colecciones en sí serían compatibles con los métodos existentes, solo las interfaces no lo serían.

Stilgar
fuente
0

Estamos haciendo una gran refactorización en este momento y nos faltaba este get () fuertemente tipado para verificar que no nos perdimos algunos get () con el tipo antiguo.

Pero encontré una solución alternativa / truco feo para la verificación del tiempo de compilación: crear una interfaz de mapa con get, contieneKey, remove ... y ponerlo en el paquete java.util de su proyecto.

Obtendrá errores de compilación solo por llamar a get (), ... con tipos incorrectos, todo lo demás parece estar bien para el compilador (al menos dentro de eclipse kepler).

No olvide eliminar esta interfaz después de verificar su compilación, ya que esto no es lo que desea en tiempo de ejecución.

Henva
fuente