Quali sono i motivi per cui Map.get (Object key) non è (completamente) generico


405

Quali sono i motivi alla base della decisione di non avere un metodo get completamente generico nell'interfaccia di java.util.Map<K, V>.

Per chiarire la domanda, la firma del metodo è

V get(Object key)

invece di

V get(K key)

e mi chiedo perché (stessa cosa per remove, containsKey, containsValue).


3
Domanda simile per quanto riguarda Collection: stackoverflow.com/questions/104799/...
AlikElzin-Kilaka


1
Sorprendente. Uso Java da oltre 20 anni e oggi mi rendo conto di questo problema.
GhostCat

Risposte:


260

Come menzionato da altri, il motivo per cui get(), ecc. Non è generico perché la chiave della voce che stai recuperando non deve essere dello stesso tipo dell'oggetto a cui passi get(); la specifica del metodo richiede solo che siano uguali. Ciò deriva da come il equals()metodo accetta un oggetto come parametro, non solo lo stesso tipo di oggetto.

Anche se può essere comunemente vero che molte classi hanno equals()definito in modo tale che i suoi oggetti possano essere uguali a quelli della propria classe, ci sono molti posti in Java in cui non è così. Ad esempio, la specifica per List.equals()dice che due oggetti Elenco sono uguali se sono entrambi Elenchi e hanno gli stessi contenuti, anche se sono implementazioni diverse di List. Quindi, tornando all'esempio in questa domanda, secondo la specifica del metodo è possibile avere un Map<ArrayList, Something>e per me chiamare get()con un LinkedListargomento as, e dovrebbe recuperare la chiave che è un elenco con lo stesso contenuto. Ciò non sarebbe possibile se get()fossero generici e limitassero il tipo di argomento.


28
Allora perché è V Get(K k)in C #?

134
La domanda è: se vuoi chiamare m.get(linkedList), perché non hai definito mil tipo di Map<List,Something>? Non riesco a pensare a un caso d'uso in cui abbia senso chiamare m.get(HappensToBeEqual)senza cambiare il Maptipo per ottenere un'interfaccia.
Elazar Leibovich,

58
Wow, grave difetto di progettazione. Nemmeno ricevi avvertimenti sul compilatore, incasinato. Sono d'accordo con Elazar. Se questo è davvero utile, cosa che dubito spesso accada, un getByEquals (Object key) sembra più ragionevole ...
mmm

37
Questa decisione sembra essere stata presa sulla base della purezza teorica piuttosto che della praticità. Per la maggior parte degli usi, gli sviluppatori preferirebbero di gran lunga vedere l'argomento limitato dal tipo di modello, piuttosto che averlo illimitato per supportare casi limite come quello menzionato da newacct nella sua risposta. Lasciare le firme non basate su modelli crea più problemi di quanti ne risolva.
Sam Goldberg,

14
@newacct: "perfect type safe" è una forte richiesta di un costrutto che può fallire in modo imprevedibile in fase di esecuzione. Non restringere la vista alle mappe di hash che funzionano con quello. TreeMappotrebbe non riuscire quando si passano oggetti di tipo errato al getmetodo, ma potrebbe passare di tanto in tanto, ad esempio quando la mappa risulta vuota. E ancora peggio, nel caso di un fornito Comparatoril comparemetodo (che ha una firma generica!) Potrebbe essere chiamato con argomenti di tipo errato senza alcun avviso non controllato. Questo è un comportamento rotto.
Holger,

105

Un fantastico programmatore Java su Google, Kevin Bourrillion, ha scritto esattamente questo problema in un post di blog qualche tempo fa (è vero che nel contesto di Setanziché Map). La frase più rilevante:

Uniformamente, i metodi di Java Collections Framework (e anche di Google Collections Library) non limitano mai i tipi dei loro parametri, tranne quando è necessario per evitare che la raccolta si rompa.

Non sono del tutto sicuro di essere d'accordo con esso come principio - .NET sembra andare bene richiedendo il giusto tipo di chiave, per esempio - ma vale la pena seguire il ragionamento nel post del blog. (Dopo aver menzionato .NET, vale la pena spiegare che parte del motivo per cui non è un problema in .NET è che c'è un problema più grande in .NET di varianza più limitata ...)


4
Apocalisp: non è vero, la situazione è sempre la stessa.
Kevin Bourrillion,

9
@ user102008 No, il post non è sbagliato. Anche se an Integere a Doublenon possono mai essere uguali tra loro, è comunque una buona domanda se a Set<? extends Number>contenga il valore new Integer(5).
Kevin Bourrillion,

33
Non ho mai voluto verificare una volta l'appartenenza a Set<? extends Foo>. Molto spesso ho cambiato il tipo di chiave di una mappa e poi sono stato frustrato dal fatto che il compilatore non riuscisse a trovare tutti i luoghi in cui il codice doveva essere aggiornato. Non sono davvero convinto che questo sia il giusto compromesso.
Porculus,

4
@EarthEngine: è sempre stato rotto. Questo è il punto: il codice è rotto, ma il compilatore non può prenderlo.
Jon Skeet,

1
Ed è ancora rotto, e ci ha appena causato un bug ... risposta fantastica.
GhostCat

28

Il contratto è così espresso:

Più formalmente, se questa mappa contiene una mappatura da una chiave k a un valore v tale che (chiave == null? K == null: key.equals (k) ), allora questo metodo restituisce v; altrimenti restituisce null. (Può esserci al massimo una di queste mappature.)

(la mia enfasi)

e come tale, una ricerca chiave riuscita dipende dall'implementazione della chiave di input del metodo di uguaglianza. Ciò non dipende necessariamente dalla classe di k.


4
Dipende anche da hashCode(). Senza una corretta implementazione di hashCode (), una buona implementazione equals()è piuttosto inutile in questo caso.
Rudolfson,

5
Immagino, in linea di principio, questo ti consentirebbe di utilizzare un proxy leggero per una chiave, se ricreare l'intera chiave fosse impraticabile, purché equals () e hashCode () siano implementati correttamente.
Bill Michell,

5
@rudolfson: per quanto ne so, solo una HashMap fa affidamento sul codice hash per trovare il bucket corretto. Una TreeMap, ad esempio, utilizza un albero di ricerca binario e non si preoccupa di hashCode ().
Rob,

4
A rigor di termini, get()non è necessario prendere un argomento di tipo Objectper soddisfare il contatto. Immagina che il metodo get fosse limitato al tipo di chiave K: il contratto sarebbe comunque valido. Naturalmente, gli usi in cui il tipo di tempo di compilazione non era una sottoclasse Knon riuscirebbero ora a compilare, ma ciò non invalida il contratto, poiché i contratti discutono implicitamente cosa succede se il codice viene compilato.
BeeOnRope,

16

È un'applicazione della Legge di Postel, "sii prudente in ciò che fai, sii liberale in ciò che accetti dagli altri".

I controlli di uguaglianza possono essere eseguiti indipendentemente dal tipo; il equalsmetodo è definito sulla Objectclasse e accetta qualsiasi Objectcome parametro. Pertanto, ha senso che l'equivalenza chiave e le operazioni basate sull'equivalenza chiave accettino qualsiasi Objecttipo.

Quando una mappa restituisce valori chiave, conserva quante più informazioni possibili sul tipo, usando il parametro type.


4
Allora perché è V Get(K k)in C #?

1
È V Get(K k)in C # perché ha anche senso. La differenza tra gli approcci Java e .NET è davvero solo chi blocca le cose non corrispondenti. In C # è il compilatore, in Java è la raccolta. Ogni tanto mi arrabbio per le incoerenti classi di raccolta di .NET, ma Get()e Remove()accettare solo un tipo di corrispondenza ti impedisce sicuramente di passare accidentalmente un valore sbagliato.
Wormbo,

26
È un'applicazione errata della Legge di Postel. Sii liberale in ciò che accetti dagli altri, ma non troppo liberale. Questa API idiota significa che non puoi distinguere tra "non nella raccolta" e "hai commesso un errore di battitura statica". Molte migliaia di ore di programmazione perse avrebbero potuto essere evitate con get: K -> booleano.
Giudice Mental,

1
Certo che avrebbe dovuto essere contains : K -> boolean.
Giudice Mental,


13

Penso che questa sezione di Generics Tutorial spieghi la situazione (la mia enfasi):

"Devi assicurarti che l'API generica non sia indebitamente restrittiva; deve continuare a supportare il contratto originale dell'API. Considera di nuovo alcuni esempi da java.util.Collection. L'API pre-generica assomiglia a:

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

Un ingenuo tentativo di generarlo è:

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

Anche se questo è sicuramente sicuro, non è all'altezza del contratto originale dell'API. Il metodo contieneAll () funziona con qualsiasi tipo di raccolta in entrata. Avrà successo solo se la raccolta in arrivo contiene davvero solo istanze di E, ma:

  • Il tipo statico della raccolta in arrivo potrebbe essere diverso, forse perché il chiamante non conosce il tipo preciso della raccolta inoltrata o forse perché si tratta di una raccolta <S>, dove S è un sottotipo di E.
  • È perfettamente legittimo chiamare IncludesAll () con una raccolta di tipo diverso. La routine dovrebbe funzionare, restituendo false. "

2
perché no containsAll( Collection< ? extends E > c )allora?
Giudice Mental,

1
@JudgeMental, anche se non riportato come esempio sopra, è anche necessario consentire containsAllcon un Collection<S>dove Sè un supertipo di E. Questo non sarebbe permesso se fosse containsAll( Collection< ? extends E > c ). Inoltre, come è esplicitamente dichiarato nell'esempio, è lecito passare una raccolta di tipo diverso (con il valore restituito quindi false).
davmac,

Non dovrebbe essere necessario consentire contieneAll con una raccolta di un supertipo di E. Sostengo che è necessario impedire tale chiamata con un controllo del tipo statico per prevenire un bug. È un contratto sciocco, che penso sia il punto della domanda originale.
Giudice Mental,

6

Il motivo è che il contenimento è determinato da equalse hashCodequali sono i metodi attivi Objected entrambi accettano un Objectparametro. Questo è stato un primo difetto di progettazione nelle librerie standard di Java. Insieme alle limitazioni nel sistema di tipi Java, impone che tutto ciò che si basa su uguale e hashCode Object.

L'unico modo per avere le tabelle hash type-safe e uguaglianza in Java è quello di astenersi Object.equalse Object.hashCodedi utilizzare un sostituto generico. Java funzionale viene fornito con classi di tipi solo per questo scopo: Hash<A>e Equal<A>. Viene HashMap<K, V>fornito un wrapper per che prende Hash<K>e Equal<K>nel suo costruttore. Questa classe gete i containsmetodi quindi prendono un argomento generico di tipo K.

Esempio:

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

4
Questo di per sé non impedisce che il tipo di 'get' venga dichiarato come "V get (tasto K)", perché "Object" è sempre un antenato di K, quindi "key.hashCode ()" sarebbe comunque valido.
finnw,

1
Anche se non lo impedisce, penso che lo spieghi. Se hanno cambiato il metodo equals per forzare l'uguaglianza di classe, di certo non potevano dire alla gente che il meccanismo sottostante per localizzare l'oggetto nella mappa utilizza equals () e hashmap () quando i prototipi del metodo per quei metodi non sono compatibili.
CG

5

Compatibilità.

Prima che i generici fossero disponibili, c'era solo get (Object o).

Se avessero cambiato questo metodo per ottenere (<K> o) avrebbe potenzialmente imposto una manutenzione massiccia del codice agli utenti java solo per far compilare nuovamente il codice funzionante.

Avrebbero potuto introdurre un metodo aggiuntivo , ad esempio get_checked (<K> o) e deprecare il vecchio metodo get () in modo che esistesse un percorso di transizione più delicato. Ma per qualche motivo, questo non è stato fatto. (La situazione in cui ci troviamo ora è che è necessario installare strumenti come findBugs per verificare la compatibilità dei tipi tra l'argomento get () e il tipo di chiave dichiarato <K> della mappa.)

Gli argomenti relativi alla semantica di .equals () sono falsi, credo. (Tecnicamente sono corretti, ma continuo a pensare che siano falsi. Nessun progettista nella sua mente giusta renderà mai o1.equals (o2) vero se o1 e o2 non hanno alcuna superclasse comune.)


4

C'è un motivo in più, non può essere fatto tecnicamente, perché rompe la Mappa.

Java ha una costruzione polimorfica generica come <? extends SomeClass>. Contrassegnato tale riferimento può puntare al tipo firmato con <AnySubclassOfSomeClass>. Ma il generico polimorfico fa quel riferimento in sola lettura . Il compilatore consente di utilizzare i tipi generici solo come tipo di metodo di restituzione (come i getter semplici), ma blocca l'utilizzo di metodi in cui il tipo generico è argomento (come i setter ordinari). Significa che se scrivi Map<? extends KeyType, ValueType>, il compilatore non ti consente di chiamare il metodo get(<? extends KeyType>)e la mappa sarà inutile. L'unica soluzione è quella di rendere questo metodo non generico: get(Object).


perché il metodo set è fortemente tipizzato allora?
Sentenza,

se intendi 'put': il metodo put () cambia mappa e non sarà disponibile con generici come <? estende SomeClass>. Se lo chiami hai un'eccezione di compilazione. Tale mappa sarà "di sola lettura"
Owheee,

1

Compatibilità all'indietro, immagino. Map(o HashMap) deve ancora supportare get(Object).


13
Ma lo stesso argomento potrebbe essere fatto per put(che limita i tipi generici). Ottieni la retrocompatibilità usando tipi non elaborati. I generici sono "opt-in".
Thilo,

Personalmente, penso che la ragione più probabile per questa decisione progettuale sia la retrocompatibilità.
geekdenz,

1

Stavo guardando questo e pensando perché l'hanno fatto in questo modo. Non credo che nessuna delle risposte esistenti spieghi perché non possano semplicemente far accettare alla nuova interfaccia generica solo il tipo corretto per la chiave. Il vero motivo è che nonostante abbiano introdotto i generici NON hanno creato una nuova interfaccia. L'interfaccia di Map è la stessa vecchia mappa non generica che serve sia come versione generica che non generica. In questo modo, se si dispone di un metodo che accetta una mappa non generica, è possibile passarlo a Map<String, Customer>e funzionerebbe comunque. Allo stesso tempo, il contratto per get accetta Object, quindi anche la nuova interfaccia dovrebbe supportare questo contratto.

A mio avviso avrebbero dovuto aggiungere una nuova interfaccia e implementare entrambe le raccolte esistenti, ma hanno deciso a favore di interfacce compatibili anche se ciò significa un design peggiore per il metodo get. Si noti che le raccolte stesse sarebbero compatibili con i metodi esistenti che solo le interfacce non avrebbero.


0

Stiamo facendo un grande refactoring proprio ora e ci mancava questo get () fortemente tipizzato per verificare che non ci siamo persi alcuni get () con il vecchio tipo.

Ma ho trovato una soluzione alternativa / brutto trucco per il controllo del tempo di compilazione: crea un'interfaccia di Map con get fortemente, contieneKey, rimuovi ... e inseriscila nel pacchetto java.util del tuo progetto.

Otterrai errori di compilazione solo per la chiamata di get (), ... con tipi errati, tutto ciò che gli altri sembrano ok per il compilatore (almeno all'interno di eclipse kepler).

Non dimenticare di eliminare questa interfaccia dopo aver verificato la tua build in quanto non è ciò che desideri in runtime.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.