Quali sono i motivi per cui Map.get (Chiave dell’object) non è (completamente) generico

Quali sono le ragioni alla base della decisione di non avere un metodo get generico nell’interfaccia di java.util.Map .

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 ).

Come menzionato da altri, il motivo per cui get() , ecc. Non è generico perché la chiave della voce che si sta recuperando non deve essere dello stesso tipo dell’object che si passa a get() ; la specifica del metodo richiede solo che siano uguali. Questo deriva dal modo in cui il metodo equals() accetta un object come parametro, non solo lo stesso tipo dell’object.

Sebbene possa essere comunemente vero che molte classi abbiano equals() definito in modo tale che i suoi oggetti possano essere uguali solo agli oggetti della sua stessa class, ci sono molti posti in Java dove questo non è il caso. Ad esempio, la specifica per List.equals() dice che due oggetti List sono uguali se sono entrambi elenchi e hanno gli stessi contenuti, anche se sono implementazioni diverse di List . Tornando all’esempio in questa domanda, secondo la specifica del metodo è ansible avere una Map e per me chiamare get() con una LinkedList come argomento, e dovrebbe recuperare la chiave che è una lista con lo stesso contenuto Questo non sarebbe ansible se get() fosse generico e limitasse il suo tipo di argomento.

Un fantastico programmatore Java su Google, Kevin Bourrillion, ha scritto esattamente su questo problema in un post del blog qualche tempo fa (ovviamente nel contesto di Set anziché di Map ). La frase più pertinente:

In modo uniforms, i metodi di Java Collections Framework (e anche la libreria di Google Collections) non restringono mai i tipi dei loro parametri, tranne quando è necessario impedire che la raccolta si guasti.

Non sono del tutto sicuro di essere d’accordo con esso come principio – .NET sembra andare bene, per esempio, ad esempio – ma vale la pena seguire il ragionamento nel post del blog. (Avendo menzionato .NET, vale la pena spiegare che parte del motivo per cui non è un problema in .NET è che c’è il problema più grande in .NET di varianza più limitata …)

Il contratto è così express:

Più formalmente, se questa mappa contiene una mapping da una chiave k ad un valore v tale che (key == null? K == null: key.equals (k) ), allora questo metodo restituisce v; altrimenti restituisce null. (Ci può essere al massimo una tale mapping.)

(la mia enfasi)

e come tale, una ricerca chiave di successo dipende dall’implementazione del metodo di uguaglianza da parte del tasto di input. Questo non dipende necessariamente dalla class di k.

È un’applicazione della legge di Postel, “sii prudente in ciò che fai, sii liberale in ciò che accetti agli altri”.

I controlli di uguaglianza possono essere eseguiti indipendentemente dal tipo; il metodo equals è definito sulla class Object e accetta qualsiasi Object come parametro. Quindi, ha senso per l’equivalenza delle chiavi e le operazioni basate sull’equivalenza delle chiavi, per accettare qualsiasi tipo di Object .

Quando una mappa restituisce valori chiave, conserva il maggior numero ansible di informazioni sul tipo, utilizzando il parametro type.

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

“È necessario accertarsi che l’API generica non sia indebitamente restrittiva, ma deve continuare a supportare il contratto originale dell’API. Riprova alcuni esempi di java.util.Collection. L’API pre-generica ha il seguente aspetto:

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

Un ingenuo tentativo di generarlo è:

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

Mentre questo è sicuramente sicuro, non è all’altezza del contratto originale dell’API. Il metodo containsAll () 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 entrata potrebbe essere diverso, forse perché il chiamante non conosce il tipo preciso della collezione che viene passata, o forse perché è una collezione , dove S è un sottotipo di E.
  • È perfettamente legittimo chiamare containsAll () con una raccolta di un tipo diverso. La routine dovrebbe funzionare, restituendo false. ”

Il motivo è che il contenimento è determinato da equals e hashCode che sono metodi su Object ed entrambi prendono un parametro Object . Questa era una delle prime pecche del design nelle librerie standard di Java. Accoppiato con limitazioni nel sistema di tipi di Java, forza tutto ciò che si basa su equals e hashCode per prendere Object .

L’unico modo per avere tabelle hash sicure per il tipo e uguaglianza in Java è di evitare Object.equals e Object.hashCode e utilizzare un sostituto generico. Java funzionale ha classi di tipi per questo scopo: Hash e Equal . Viene HashMap un wrapper per HashMap che accetta Hash e Equal nel suo costruttore. I metodi get e contains questa class accettano quindi un argomento generico di tipo K

Esempio:

 HashMap h = new HashMap(Equal.stringEqual, Hash.stringHash); h.add("one", 1); h.get("one"); // All good h.get(Integer.valueOf(1)); // Compiler error 

C’è un altro motivo pesante, non può essere fatto tecnicamente, perché rompe la mappa.

Java ha una costruzione generica polimorfa come . Contrassegnato tale riferimento può puntare al tipo firmato con . Ma il generico polimorfico rende questo riferimento in sola lettura . Il compilatore consente di utilizzare tipi generici solo come metodo di restituzione del metodo (come getter semplici), ma blocca l’utilizzo di metodi in cui il tipo generico è argomento (come setter ordinari). Significa che scrivi Map Map , il compilatore non ti permette di chiamare il metodo get() , e la mappa sarà inutile. L’unica soluzione è rendere questo metodo non generico: get(Object) .

Compatibilità con le versioni precedenti, immagino. Map (o HashMap ) deve ancora supportare get(Object) .

Stavo guardando questo e pensando perché lo hanno fatto in questo modo. Non penso che nessuna delle risposte esistenti spieghi perché non potrebbero semplicemente rendere la nuova interfaccia generica accettata solo il tipo corretto per la chiave. La vera ragione è che anche se hanno introdotto i generici NON hanno creato una nuova interfaccia. L’interfaccia della mappa è la stessa vecchia mappa non generica che serve solo come versione generica e non generica. In questo modo se hai un metodo che accetta Map non generico puoi passarlo a Map e funzionerebbe ancora. Allo stesso tempo il contratto per ottenere accetta object, quindi la nuova interfaccia dovrebbe supportare anche questo contratto.

Secondo me avrebbero dovuto aggiungere una nuova interfaccia e implementarle entrambe nella collezione esistente, ma hanno deciso in favore di interfacce compatibili, anche se ciò comporta un design peggiore per il metodo get. Si noti che le raccolte stesse sarebbero compatibili con i metodi esistenti solo le interfacce non lo farebbero.

Compatibilità.

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

Se avessero cambiato questo metodo per ottenere ( o), avrebbe potenzialmente forzato la massiccia manutenzione del codice sugli utenti java solo per rendere di nuovo compilato il codice funzionante.

Avrebbero potuto introdurre un metodo aggiuntivo , ad esempio get_checked ( o) e deprecare il vecchio metodo get () in modo che esistesse un percorso di transizione più delicato. Ma per qualche ragione, 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 dichiarata della mappa.)

Gli argomenti relativi alla semantica di .equals () sono falsi, penso. (Tecnicamente sono corretti, ma continuo a pensare che siano falsi. Nessun progettista sano di mente farà mai o1.equals (o2) true se o1 e o2 non hanno alcuna superclass comune.)

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

Ma ho trovato un trucco workaround / brutto per il controllo del tempo di compilazione: crea un’interfaccia Map con tip tipicamente get, containsKey, remove … e mettilo nel pacchetto java.util del tuo progetto.

Riceverai errori di compilazione solo per chiamare get (), … con tipi errati, tutto il resto sembra ok per il compilatore (almeno all’interno di eclipse kepler).

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