Perché le raccolte Java non rimuovono i metodi generici?


144

Perché Collection.remove (Object o) non è generico?

Sembra che Collection<E>avrebbe potutoboolean remove(E o);

Quindi, quando si tenta di rimuovere accidentalmente (ad esempio) Set<String>invece di ogni singola stringa da a Collection<String>, si tratterebbe di un errore di compilazione anziché di un problema di debug in un secondo momento.


13
Questo può davvero morderti quando lo combini con l'autoboxing. Se provi a rimuovere qualcosa da un elenco e gli passi un numero intero anziché un int, chiama il metodo remove (Object).
ScArcher2,

2
Domanda simile per quanto riguarda Mappa: stackoverflow.com/questions/857420/...
AlikElzin-Kilaka

Risposte:


73

Josh Bloch e Bill Pugh fanno riferimento a questo problema in Java Puzzlers IV: The Phantom Reference Menace, Attack of the Clone e Revenge of The Shift .

Josh Bloch dice (6:41) che hanno tentato di generare il metodo get di Map, rimuovere method e altri, ma "semplicemente non ha funzionato".

Esistono troppi programmi ragionevoli che non possono essere generati se si consente solo il tipo generico della raccolta come tipo di parametro. L'esempio dato da lui è un'intersezione di a Listdi Numbers e Listdi di Longs.


6
Perché add () può prendere un parametro tipizzato ma remove () non è ancora un po 'al di là della mia comprensione. Josh Bloch sarebbe il riferimento definitivo per le domande sulle Collezioni. Potrebbe essere tutto ciò che ottengo senza provare a realizzare un'implementazione di raccolta simile e vedere di persona. :( Grazie.
Chris Mazzola,

2
Chris: leggi il PDF Tutorial di Java Generics, spiegherà perché.
JeeBee,

42
In realtà, è molto semplice! Se add () prendesse un oggetto sbagliato, si spezzerebbe la raccolta. Conterrebbe cose che non dovrebbero! Questo non è il caso di remove (), o contiene ().
Kevin Bourrillion,

12
Per inciso, quella regola di base, che utilizza parametri di tipo per prevenire il danno effettivo solo alla raccolta, viene seguita in modo assolutamente coerente nell'intera libreria.
Kevin Bourrillion,

3
@KevinBourrillion: Ho lavorato con i generici per anni (sia in Java che in C #) senza mai rendermi conto che esiste persino la regola del "danno reale" ... ma ora che l'ho vista dichiarare direttamente ha perfettamente il senso del 100%. Grazie per il pesce !!! Tranne ora mi sento in dovere di tornare indietro e guardare le mie implementazioni, per vedere se alcuni metodi possono e quindi dovrebbero essere degenerati. Sospiro.
corlettk,

74

remove()(in Mape in Collection) non è generico perché dovresti essere in grado di passare qualsiasi tipo di oggetto a remove(). L'oggetto rimosso non deve essere dello stesso tipo dell'oggetto a cui si passa remove(); richiede solo che siano uguali. Dalla specifica di remove(), remove(o)rimuove l'oggetto etale che (o==null ? e==null : o.equals(e))è true. Nota che non c'è nulla che richiede oe edi essere dello stesso tipo. Ciò deriva dal fatto che il equals()metodo accetta un Objectparametro as, non solo lo stesso tipo dell'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, non è certamente sempre 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, è possibile avere un Map<ArrayList, Something>e per me chiamare remove()con un LinkedListargomento as, e dovrebbe rimuovere la chiave che è un elenco con lo stesso contenuto. Ciò non sarebbe possibile se remove()fossero generici e limitassero il tipo di argomento.


1
Ma se dovessi definire la Mappa come Mappa <Elenco, Qualcosa> (anziché ArrayList), sarebbe possibile rimuoverlo utilizzando un LinkedList. Penso che questa risposta sia errata.
AlikElzin-Kilaka,

3
La risposta sembra essere corretta, ma incompleta. Si presta solo a chiedersi perché diavolo non hanno anche generalizzato il equals()metodo? Potrei vedere più vantaggi nel digitare sicurezza invece di questo approccio "libertario". Penso che la maggior parte dei casi con l'attuale implementazione riguardi l'inserimento di bug nel nostro codice piuttosto che la gioia che questa flessibilità remove()offre.
kellogs,

2
@kellogs: Cosa vorresti dire con "generalizzare il equals()metodo"?
newacct,

5
@MattBall: "dove T è la classe dichiarante" Ma non esiste una sintassi simile in Java. Tdeve essere dichiarato come parametro di tipo sulla classe e Objectnon ha alcun parametro di tipo. Non è possibile avere un tipo che faccia riferimento alla "classe dichiarante".
newacct

3
Penso che Kellogs stia dicendo cosa succede se l'uguaglianza fosse un'interfaccia generica Equality<T>con equals(T other). Quindi potresti avere remove(Equality<T> o)ed oè solo un oggetto che può essere paragonato a un altro T.
Weston,

11

Perché se il parametro type è un carattere jolly, non è possibile utilizzare un metodo di rimozione generico.

Mi sembra di aver incontrato questa domanda con il metodo get (Object) di Map. Il metodo get in questo caso non è generico, anche se dovrebbe ragionevolmente prevedere di passare un oggetto dello stesso tipo del primo parametro di tipo. Mi sono reso conto che se stai passando su Maps con un carattere jolly come primo parametro di tipo, allora non c'è modo di estrarre un elemento dalla Mappa con quel metodo, se quell'argomento fosse generico. Gli argomenti con caratteri jolly non possono davvero essere soddisfatti, poiché il compilatore non può garantire che il tipo sia corretto. Suppongo che il motivo per cui l'aggiunta sia generica sia che ci si aspetta che il tipo sia corretto prima di aggiungerlo alla raccolta. Tuttavia, quando si rimuove un oggetto, se il tipo non è corretto, non corrisponderà comunque a nulla.

Probabilmente non l'ho spiegato molto bene, ma mi sembra abbastanza logico.


1
Potresti approfondire un po 'questo?
Thomas Owens,

6

Oltre alle altre risposte, c'è un'altra ragione per cui il metodo dovrebbe accettare un Object, che è predicati. Considera il seguente esempio:

class Person {
    public String name;
    // override equals()
}
class Employee extends Person {
    public String company;
    // override equals()
}
class Developer extends Employee {
    public int yearsOfExperience;
    // override equals()
}

class Test {
    public static void main(String[] args) {
        Collection<? extends Person> people = new ArrayList<Employee>();
        // ...

        // to remove the first employee with a specific name:
        people.remove(new Person(someName1));

        // to remove the first developer that matches some criteria:
        people.remove(new Developer(someName2, someCompany, 10));

        // to remove the first employee who is either
        // a developer or an employee of someCompany:
        people.remove(new Object() {
            public boolean equals(Object employee) {
                return employee instanceof Developer
                    || ((Employee) employee).company.equals(someCompany);
        }});
    }
}

Il punto è che l'oggetto che viene passato al removemetodo è responsabile della definizione del equalsmetodo. Costruire predicati diventa molto semplice in questo modo.


Investitore? (Filler filler filler)
Matt R

3
L'elenco è implementato come yourObject.equals(developer)documentato nell'API Collezioni: java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly

13
Mi sembra un abuso
RAY

7
È un abuso poiché l'oggetto predicato rompe il contratto del equalsmetodo, vale a dire la simmetria. Il metodo remove è legato alle sue specifiche solo se i tuoi oggetti soddisfano le specifiche equals / hashCode, quindi qualsiasi implementazione sarebbe libera di fare il confronto al contrario. Inoltre, l'oggetto predicato non implementa il .hashCode()metodo (non può essere implementato in modo coerente su uguale), quindi la chiamata di rimozione non funzionerà mai su una raccolta basata su Hash (come HashSet o HashMap.keys ()). Che funzioni con ArrayList è pura fortuna.
Paŭlo Ebermann,

3
(Non sto discutendo la domanda di tipo generico - questa è stata risolta in precedenza -, solo il tuo uso di uguale per i predicati qui.) Naturalmente HashMap e HashSet stanno controllando il codice hash e TreeSet / Map stanno usando l'ordinamento degli elementi . Tuttavia, implementano completamente Collection.remove, senza rompere il contratto (se l'ordinamento è coerente con uguale a). E un ArrayList vario (o AbstractCollection, penso) con la chiamata uguale eguagliata implementerebbe ancora correttamente il contratto - è colpa tua se non funziona come previsto, poiché stai rompendo il equalscontratto.
Paŭlo Ebermann,

5

Assumere uno ha una collezione di Cat, e alcuni riferimenti a oggetti di tipi Animal, Cat, SiameseCat, e Dog. Chiedere alla raccolta se contiene l'oggetto a cui fa riferimento il riferimento Cato SiameseCatsembra ragionevole. Chiedere se contiene l'oggetto a cui fa Animalriferimento il riferimento può sembrare complicato, ma è comunque perfettamente ragionevole. Dopotutto, l'oggetto in questione potrebbe essere a Cate potrebbe apparire nella raccolta.

Inoltre, anche se l'oggetto sembra essere qualcosa di diverso da un Cat, non c'è alcun problema a dire se appare nella raccolta - rispondi semplicemente "no, non lo fa". Una raccolta di tipo "lookup" dovrebbe essere in grado di accettare in modo significativo il riferimento di qualsiasi supertipo e determinare se l'oggetto esiste all'interno della raccolta. Se il riferimento all'oggetto passato è di tipo non correlato, non è possibile che la raccolta possa contenerlo, quindi la query non è in alcun senso significativa (risponderà sempre "no"). Tuttavia, poiché non esiste alcun modo per limitare i parametri ai sottotipi o ai supertipi, è molto pratico semplicemente accettare qualsiasi tipo e rispondere "no" per qualsiasi oggetto il cui tipo non è correlato a quello della raccolta.


1
"Se il riferimento all'oggetto passato è di tipo non correlato, non è possibile che la raccolta lo contenga" Wrong. Deve contenere solo qualcosa di uguale ad esso; e oggetti di classi diverse possono essere uguali.
newacct

"può sembrare complicato, ma è ancora perfettamente ragionevole" È vero? Considera un mondo in cui un oggetto di un tipo non può sempre essere verificato per la parità con un oggetto di un altro tipo, perché a quale tipo può essere uguale è parametrizzato (simile a come Comparableè parametrizzato per i tipi con cui puoi confrontare). Quindi non sarebbe ragionevole consentire alle persone di passare qualcosa di un tipo non correlato.
newacct

@newacct: Esiste una differenza fondamentale tra confronto di magnitudo e confronto di uguaglianza: oggetti dati Ae Bdi un tipo, e Xe Ydi un altro, tale che A> B, e X> Y. In entrambi i A> Ye Y< A, oX > Be B< X. Tali relazioni possono esistere solo se i confronti di grandezza conoscono entrambi i tipi. Al contrario, il metodo di confronto di uguaglianza di un oggetto può semplicemente dichiararsi ineguale rispetto a qualsiasi altro tipo, senza dover sapere nulla dell'altro tipo in questione. Un oggetto di tipo Catpotrebbe non avere idea se sia ...
supercat

... "maggiore di" o "minore di" un oggetto di tipo FordMustang, ma non dovrebbe avere difficoltà a dire se è uguale a tale oggetto (la risposta, ovviamente, essendo "no").
supercat,

4

Ho sempre pensato che fosse perché remove () non ha motivo di preoccuparsi del tipo di oggetto che gli dai. È abbastanza facile, a prescindere, controllare se quell'oggetto è uno di quelli contenuti nella Raccolta, poiché può chiamare equals () su qualsiasi cosa. È necessario controllare il tipo su add () per assicurarsi che contenga solo oggetti di quel tipo.


0

È stato un compromesso. Entrambi gli approcci hanno il loro vantaggio:

  • remove(Object o)
    • è più flessibile. Ad esempio, consente di scorrere un elenco di numeri e rimuoverli da un elenco di long.
    • il codice che utilizza questa flessibilità può essere più facilmente generato
  • remove(E e) porta maggiore sicurezza del tipo a ciò che la maggior parte dei programmi vuole fare rilevando bug sottili in fase di compilazione, come tentare erroneamente di rimuovere un numero intero da un elenco di cortocircuiti.

La compatibilità con le versioni precedenti era sempre un obiettivo importante durante l'evoluzione dell'API Java, pertanto è stato scelto remove (Object o) perché ha reso più semplice la generazione di codice esistente. Se la compatibilità con le versioni precedenti NON fosse stata un problema, suppongo che i progettisti avrebbero scelto di rimuovere (E e).


-1

Rimuovere non è un metodo generico, pertanto il codice esistente che utilizza una raccolta non generica verrà comunque compilato e avrà lo stesso comportamento.

Vedere http://www.ibm.com/developerworks/java/library/j-jtp01255.html per i dettagli.

Modifica: un commentatore chiede perché il metodo di aggiunta sia generico. [... rimosso la mia spiegazione ...] Il secondo commentatore ha risposto alla domanda di Firebird84 molto meglio di me.


2
Allora perché il metodo add è generico?
Bob Gettys,

@ firebird84 remove (Object) ignora gli oggetti di tipo errato, ma remove (E) provocherebbe un errore di compilazione. Ciò cambierebbe il comportamento.
noah,

: scrollata di spalle: - il comportamento in fase di esecuzione non è cambiato; errore di compilazione non è comportamento di runtime . Il "comportamento" del metodo di aggiunta cambia in questo modo.
Jason S,

-2

Un altro motivo è dovuto alle interfacce. Ecco un esempio per mostrarlo:

public interface A {}

public interface B {}

public class MyClass implements A, B {}

public static void main(String[] args) {
   Collection<A> collection = new ArrayList<>();
   MyClass item = new MyClass();
   collection.add(item);  // works fine
   B b = item; // valid
   collection.remove(b); /* It works because the remove method accepts an Object. If it was generic, this would not work */
}

Stai mostrando qualcosa con cui puoi cavartela perché remove()non è covariante. La domanda, tuttavia, è se questo dovrebbe essere permesso. ArrayList#remove()funziona attraverso l'uguaglianza di valore, non l'uguaglianza di riferimento. Perché ti aspetti che a Bsia uguale a an A? Nel tuo esempio, può essere, ma è una strana aspettativa. Preferirei vederti fornire una MyClassdiscussione qui.
seh,

-3

Perché romperebbe il codice esistente (pre-Java5). per esempio,

Set stringSet = new HashSet();
// do some stuff...
Object o = "foobar";
stringSet.remove(o);

Ora potresti dire che il codice sopra è sbagliato, ma supponi che o provenga da un insieme eterogeneo di oggetti (cioè conteneva stringhe, numero, oggetti, ecc.). Vuoi rimuovere tutte le corrispondenze, il che era legale perché rimuovere avrebbe semplicemente ignorato le non stringhe perché non uguali. Ma se lo fai rimuovi (String o), non funziona più.


4
Se avessi un'istanza di List <String> mi aspetterei di poter chiamare List.remove (someString); Se devo supportare la compatibilità con le versioni precedenti, utilizzerei un elenco non elaborato - Elenco <?>, Quindi posso chiamare list.remove (someObject), no?
Chris Mazzola,

5
Se sostituisci "rimuovi" con "aggiungi", quel codice è altrettanto rotto da ciò che è stato effettivamente fatto in Java 5.
DJClayworth,
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.