¿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
)
java
generics
collections
map
WMR
fuente
fuente
Respuestas:
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 pasaget()
; La especificación del método solo requiere que sean iguales. Esto se deduce de cómo elequals()
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 deList.equals()
dice que dos objetos List son iguales si ambas son Listas y tienen el mismo contenido, incluso si son implementaciones diferentes deList
. Volviendo al ejemplo en esta pregunta, de acuerdo con la especificación del método, es posible tener unMap<ArrayList, Something>
y para que llameget()
con unLinkedList
argumento como, y debería recuperar la clave, que es una lista con el mismo contenido. Esto no sería posible siget()
fuera genérico y restringiera su tipo de argumento.fuente
V Get(K k)
en C #?m.get(linkedList)
, ¿por qué no definióm
el tipo comoMap<List,Something>
? No puedo pensar en un caso de uso donde llamarm.get(HappensToBeEqual)
sin cambiar elMap
tipo para obtener una interfaz tiene sentido.TreeMap
puede fallar cuando pasa objetos del tipo incorrecto alget
método, pero puede pasar ocasionalmente, por ejemplo, cuando el mapa está vacío. Y lo que es peor, en el caso de que se proporcioneComparator
elcompare
mé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.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
Set
lugar deMap
). La oración más relevante: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 ...)
fuente
Integer
y aDouble
nunca pueden ser iguales entre sí, sigue siendo una pregunta justa si aSet<? extends Number>
contiene el valornew Integer(5)
.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.El contrato se expresa así:
(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.
fuente
hashCode()
. Sin una implementación adecuada de hashCode (), una implementación bien implementadaequals()
es bastante inútil en este caso.get()
no necesita tomar un argumento de tipoObject
para satisfacer el contacto. Imagine que el método get se restringiera al tipo de claveK
: el contrato aún sería válido. Por supuesto, los usos donde el tipo de tiempo de compilación no era una subclase deK
ahora 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.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
equals
método se define en laObject
clase y acepta cualquieraObject
como parámetro. Por lo tanto, tiene sentido que la equivalencia clave y las operaciones basadas en la equivalencia clave acepten cualquierObject
tipo.Cuando un mapa devuelve valores clave, conserva tanta información de tipo como sea posible, utilizando el parámetro de tipo.
fuente
V Get(K k)
en C #?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, peroGet()
yRemove()
aceptando sólo un tipo de juego sin duda le impide pasar accidentalmente a un valor incorrecto en.contains : K -> boolean
.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í:
Un ingenuo intento de generarlo es:
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:
fuente
containsAll( Collection< ? extends E > c )
entonces?containsAll
con unCollection<S>
dondeS
es un supertipo deE
. Esto no estaría permitido si lo fueracontainsAll( 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 entoncesfalse
).La razón es que la contención está determinada por
equals
yhashCode
cuáles son los métodosObject
y ambos toman unObject
pará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 hashCodeObject
.La única manera de tener tablas hash con seguridad de tipos y la igualdad en Java es a evitar
Object.equals
yObject.hashCode
y utilizar un sustituto genérico. Java funcional viene con clases de tipo solo para este propósito:Hash<A>
yEqual<A>
. SeHashMap<K, V>
proporciona un contenedor para que tomaHash<K>
yEqual<K>
en su constructor. Por lo tanto, esta claseget
ycontains
métodos toman un argumento genérico de tipoK
.Ejemplo:
fuente
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).
fuente
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 escribeMap<? extends KeyType, ValueType>
, el compilador no le permite llamar al métodoget(<? extends KeyType>)
, y el mapa será inútil. La única solución es hacer que este método no genérico:get(Object)
.fuente
Compatibilidad con versiones anteriores, supongo.
Map
(oHashMap
) todavía necesita apoyoget(Object)
.fuente
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".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.
fuente
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.
fuente