Warum entfernen Java Collections keine generischen Methoden?

144

Warum ist Collection.remove (Object o) nicht generisch?

Scheint wie Collection<E>könnte habenboolean remove(E o);

Wenn Sie dann versehentlich versuchen, (zum Beispiel) Set<String>anstelle jeder einzelnen Zeichenfolge aus a zu entfernen Collection<String>, handelt es sich um einen Fehler bei der Kompilierung anstelle eines späteren Debugging-Problems.

Chris Mazzola
quelle
13
Dies kann Sie wirklich beißen, wenn Sie es mit Autoboxing kombinieren. Wenn Sie versuchen, etwas aus einer Liste zu entfernen, und eine Ganzzahl anstelle eines Int übergeben, wird die Methode remove (Object) aufgerufen.
ScArcher2
2
Ähnliche Frage bezüglich Karte: stackoverflow.com/questions/857420/…
AlikElzin-kilaka

Antworten:

73

Josh Bloch und Bill Pugh verweisen auf dieses Problem in Java Puzzlers IV: Die Phantomreferenzbedrohung, Angriff des Klons und Rache der Verschiebung .

Josh Bloch sagt (6:41), dass sie versucht haben, die get-Methode von Map zu generieren, die Methode zu entfernen und einige andere, aber "es hat einfach nicht funktioniert".

Es gibt zu viele sinnvolle Programme, die nicht generiert werden könnten, wenn Sie nur den generischen Typ der Sammlung als Parametertyp zulassen. Das von ihm gegebene Beispiel ist ein Schnittpunkt von a Listvon Numbers und a Listvon Longs.

dmeister
quelle
6
Warum add () einen eingegebenen Parameter annehmen kann, remove () jedoch nicht, liegt immer noch etwas außerhalb meines Verständnisses. Josh Bloch wäre die endgültige Referenz für Sammlungsfragen. Es könnte alles sein, was ich bekomme, ohne zu versuchen, eine ähnliche Sammlungsimplementierung vorzunehmen und mich selbst davon zu überzeugen. :( Danke.
Chris Mazzola
2
Chris - lies das PDF mit dem Java Generics Tutorial, es wird erklären warum.
JeeBee
42
Eigentlich ist es sehr einfach! Wenn add () ein falsches Objekt verwendet, wird die Auflistung beschädigt. Es würde Dinge enthalten, die es nicht soll! Dies ist bei remove () nicht der Fall oder enthält ().
Kevin Bourrillion
12
Im Übrigen wird diese Grundregel - die Verwendung von Typparametern, um nur eine tatsächliche Beschädigung der Sammlung zu verhindern - in der gesamten Bibliothek absolut konsequent befolgt.
Kevin Bourrillion
3
@ KevinBourrillion: Ich arbeite seit Jahren mit Generika (sowohl in Java als auch in C #), ohne jemals zu bemerken, dass es überhaupt eine Regel für "tatsächlichen Schaden" gibt ... aber jetzt, wo ich sie direkt gesehen habe, ist sie zu 100% vollkommen sinnvoll. Danke für den Fisch !!! Außer jetzt fühle ich mich gezwungen, zurück zu gehen und meine Implementierungen zu betrachten, um zu sehen, ob einige Methoden degeneriert werden können und sollten. Seufzer.
Corlettk
74

remove()(in Mapsowie in Collection) ist nicht generisch, da Sie in der Lage sein sollten, jede Art von Objekt an zu übergeben remove(). Das entfernte Objekt muss nicht vom selben Typ sein wie das Objekt, an das Sie übergeben remove(). es erfordert nur, dass sie gleich sind. Aus der Beschreibung der remove(), remove(o)entfernt das Objekt , eso dass (o==null ? e==null : o.equals(e))ist true. Beachten Sie, dass nichts erforderlich ist ound evom selben Typ sein muss. Dies folgt aus der Tatsache, dass die equals()Methode einen Objectas-Parameter akzeptiert, nicht nur denselben Typ wie das Objekt.

Obwohl es allgemein zutreffend sein mag, dass viele Klassen so equals()definiert haben, dass ihre Objekte nur Objekten seiner eigenen Klasse entsprechen können, ist dies sicherlich nicht immer der Fall. In der Spezifikation für List.equals()heißt es beispielsweise, dass zwei Listenobjekte gleich sind, wenn sie beide Listen sind und denselben Inhalt haben, auch wenn es sich um unterschiedliche Implementierungen von handelt List. Wenn wir also auf das Beispiel in dieser Frage zurückkommen, ist es möglich, ein Map<ArrayList, Something>und für mich remove()mit einem LinkedListas-Argument aufzurufen , und es sollte den Schlüssel entfernen, der eine Liste mit demselben Inhalt ist. Dies wäre nicht möglich, wenn remove()der Argumenttyp generisch und eingeschränkt wäre.

newacct
quelle
1
Wenn Sie die Map jedoch als Map <List, Something> (anstelle von ArrayList) definieren würden, wäre es möglich gewesen, sie mithilfe einer LinkedList zu entfernen. Ich denke, diese Antwort ist falsch.
AlikElzin-Kilaka
3
Die Antwort scheint richtig, aber unvollständig zu sein. Es bietet sich nur an zu fragen, warum zum Teufel sie die equals()Methode nicht auch generisiert haben . Ich könnte mehr Vorteile für die Typensicherheit sehen als für diesen "libertären" Ansatz. Ich denke, die meisten Fälle mit der aktuellen Implementierung sind eher auf Fehler zurückzuführen, die in unseren Code gelangen, als auf die Freude an dieser Flexibilität, die die remove()Methode mit sich bringt.
Kellogs
2
@kellogs: Was würdest du mit "generize the equals()method" meinen ?
Newacct
5
@MattBall: "wobei T die deklarierende Klasse ist" Aber in Java gibt es keine solche Syntax. Tmuss als Typparameter in der Klasse deklariert werden und Objecthat keine Typparameter. Es gibt keine Möglichkeit, einen Typ zu haben, der sich auf "die deklarierende Klasse" bezieht.
Newacct
3
Ich denke , kellogs sagt was , wenn Gleichheit eine generische Schnittstelle waren Equality<T>mit equals(T other). Dann könnten remove(Equality<T> o)und haben Sie onur ein Objekt, das mit einem anderen verglichen werden kann T.
Weston
11

Wenn Ihr Typparameter ein Platzhalter ist, können Sie keine generische Entfernungsmethode verwenden.

Ich erinnere mich an diese Frage mit der get (Object) -Methode von Map. Die get-Methode ist in diesem Fall nicht generisch, sollte jedoch vernünftigerweise erwarten, dass ein Objekt des gleichen Typs wie der erste Typparameter übergeben wird. Ich habe festgestellt, dass es mit dieser Methode keine Möglichkeit gibt, ein Element aus der Map zu entfernen, wenn Sie Maps mit einem Platzhalter als erstem Typparameter übergeben, wenn dieses Argument generisch ist. Platzhalterargumente können nicht wirklich erfüllt werden, da der Compiler nicht garantieren kann, dass der Typ korrekt ist. Ich spekuliere, dass der Grund, warum add generisch ist, darin besteht, dass von Ihnen erwartet wird, dass Sie sicherstellen, dass der Typ korrekt ist, bevor Sie ihn der Sammlung hinzufügen. Wenn beim Entfernen eines Objekts der Typ jedoch falsch ist, stimmt er ohnehin mit nichts überein.

Ich habe es wahrscheinlich nicht sehr gut erklärt, aber es scheint mir logisch genug.

Bob Gettys
quelle
1
Könnten Sie das etwas näher erläutern?
Thomas Owens
6

Zusätzlich zu den anderen Antworten gibt es einen weiteren Grund, warum die Methode ein ObjectPrädikat akzeptieren sollte . Betrachten Sie das folgende Beispiel:

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);
        }});
    }
}

Der Punkt ist, dass das Objekt, das an die removeMethode übergeben wird, für die Definition der equalsMethode verantwortlich ist. Das Erstellen von Prädikaten wird auf diese Weise sehr einfach.

Hosam Aly
quelle
Investor? (Füllstoff Füller)
Matt R
3
Die Liste wird wie yourObject.equals(developer)in der Sammlungs-API dokumentiert implementiert : java.sun.com/javase/6/docs/api/java/util/…
Hosam Aly
13
Dies scheint mir ein Missbrauch zu sein
RAY
7
Es ist ein Missbrauch, da Ihr Prädikatobjekt den Vertrag der equalsMethode, nämlich die Symmetrie, bricht . Die Methode remove ist nur an ihre Spezifikation gebunden, solange Ihre Objekte die Spezifikation equals / hashCode erfüllen. Daher kann jede Implementierung den Vergleich auch umgekehrt durchführen. Außerdem implementiert Ihr Prädikatobjekt die .hashCode()Methode nicht (kann nicht konsistent auf gleich implementiert werden), sodass der Aufruf zum Entfernen niemals für eine Hash-basierte Auflistung (wie HashSet oder HashMap.keys ()) funktioniert. Dass es mit ArrayList funktioniert, ist reines Glück.
Paŭlo Ebermann
3
(Ich diskutiere nicht die generische Typfrage - dies wurde zuvor beantwortet -, nur Ihre Verwendung von Gleichen für Prädikate hier.) Natürlich suchen HashMap und HashSet nach dem Hash-Code, und TreeSet / Map verwenden die Reihenfolge der Elemente . Sie werden jedoch vollständig umgesetzt Collection.remove, ohne den Vertrag zu brechen (wenn die Bestellung gleich ist). Und eine abwechslungsreiche ArrayList (oder AbstractCollection, glaube ich) mit dem gleichgestellten Aufruf würde den Vertrag immer noch korrekt implementieren - es ist Ihre Schuld, wenn er nicht wie beabsichtigt funktioniert, da Sie den equalsVertrag brechen .
Paŭlo Ebermann
5

Angenommen , man eine Sammlung von hat Cat, und einige Objektreferenzen von Typen Animal, Cat, SiameseCat, und Dog. Es erscheint vernünftig, die Sammlung zu fragen, ob sie das Objekt enthält, auf das sich die Referenz Catoder die SiameseCatReferenz bezieht. Die Frage, ob es das Objekt enthält, auf das sich die AnimalReferenz bezieht, mag zweifelhaft erscheinen, ist aber dennoch durchaus vernünftig. Das fragliche Objekt könnte schließlich ein sein Catund in der Sammlung erscheinen.

Selbst wenn das Objekt etwas anderes als a ist Cat, ist es kein Problem zu sagen, ob es in der Sammlung erscheint - antworten Sie einfach mit "Nein, tut es nicht". Eine Sammlung im "Lookup-Stil" eines Typs sollte in der Lage sein, Verweise auf einen beliebigen Supertyp sinnvoll zu akzeptieren und festzustellen, ob das Objekt in der Sammlung vorhanden ist. Wenn die übergebene Objektreferenz von einem nicht verwandten Typ ist, kann die Sammlung sie möglicherweise nicht enthalten, sodass die Abfrage in gewissem Sinne nicht aussagekräftig ist (sie antwortet immer mit "Nein"). Da es jedoch keine Möglichkeit gibt, Parameter auf Subtypen oder Supertypen zu beschränken, ist es am praktischsten, einfach einen beliebigen Typ zu akzeptieren und für Objekte, deren Typ nicht mit dem der Sammlung zusammenhängt, mit "Nein" zu antworten.

Superkatze
quelle
1
"Wenn die übergebene Objektreferenz von einem nicht verwandten Typ ist, kann die Sammlung sie möglicherweise nicht enthalten." Falsch. Es muss nur etwas Gleiches enthalten; und Objekte verschiedener Klassen können gleich sein.
Newacct
"mag zwielichtig erscheinen, aber es ist immer noch völlig vernünftig" Ist es? Stellen Sie sich eine Welt vor, in der ein Objekt eines Typs nicht immer auf Gleichheit mit einem Objekt eines anderen Typs überprüft werden kann, da der Typ, dem es entsprechen kann, parametrisiert wird (ähnlich wie Comparablebei den Typen, mit denen Sie vergleichen können). Dann wäre es nicht vernünftig, Menschen zu erlauben, etwas von einem nicht verwandten Typ zu übergeben.
Newacct
@newacct: Es gibt einen grundlegenden Unterschied zwischen Größenvergleich und Gleichheitsvergleich: gegebene Objekte Aund Bvon einem Typ und Xund Yvon einem anderen, so dass A> Bund X> Y. Entweder A> Yund Y< Aoder X> Bund B< X. Diese Beziehungen können nur bestehen, wenn die Größenvergleiche beide Typen kennen. Im Gegensatz dazu kann sich die Gleichheitsvergleichsmethode eines Objekts einfach für ungleich erklären, ohne dass irgendetwas über den anderen fraglichen Typ bekannt sein muss. Ein Objekt vom Typ hat Catmöglicherweise keine Ahnung, ob es ...
Supercat
... "größer als" oder "kleiner als" ein Objekt vom Typ FordMustang, aber es sollte keine Schwierigkeit geben zu sagen, ob es einem solchen Objekt entspricht (die Antwort lautet offensichtlich "nein").
Supercat
4

Ich habe immer gedacht, dass dies daran liegt, dass remove () keinen Grund hat, sich darum zu kümmern, welche Art von Objekt Sie ihm geben. Unabhängig davon ist es einfach genug zu überprüfen, ob dieses Objekt eines der Objekte ist, die in der Sammlung enthalten sind, da es für alles equals () aufrufen kann. Es ist erforderlich, den Typ bei add () zu überprüfen, um sicherzustellen, dass nur Objekte dieses Typs enthalten sind.

ColinD
quelle
0

Es war ein Kompromiss. Beide Ansätze haben ihren Vorteil:

  • remove(Object o)
    • ist flexibler. Zum Beispiel können Sie eine Liste von Zahlen durchlaufen und sie aus einer Liste von Longs entfernen.
    • Code, der diese Flexibilität nutzt, kann einfacher generiert werden
  • remove(E e) bringt mehr Typensicherheit für das, was die meisten Programme tun möchten, indem sie beim Kompilieren subtile Fehler erkennen, z. B. den irrtümlichen Versuch, eine Ganzzahl aus einer Liste von Kurzschlüssen zu entfernen.

Die Abwärtskompatibilität war bei der Entwicklung der Java-API immer ein wichtiges Ziel. Daher wurde remove (Object o) ausgewählt, da dies das Generieren von vorhandenem Code erleichtert. Wenn die Abwärtskompatibilität KEIN Problem gewesen wäre, hätten die Designer vermutlich remove (E e) gewählt.

Stefan Feuerhahn
quelle
-1

Entfernen ist keine generische Methode, sodass vorhandener Code, der eine nicht generische Sammlung verwendet, weiterhin kompiliert wird und dasselbe Verhalten aufweist.

Weitere Informationen finden Sie unter http://www.ibm.com/developerworks/java/library/j-jtp01255.html .

Bearbeiten: Ein Kommentator fragt, warum die Add-Methode generisch ist. [... meine Erklärung entfernt ...] Der zweite Kommentator beantwortete die Frage von firebird84 viel besser als ich.

Jeff C.
quelle
2
Warum ist die Add-Methode dann generisch?
Bob Gettys
@ firebird84 remove (Object) ignoriert Objekte des falschen Typs, remove (E) würde jedoch einen Kompilierungsfehler verursachen. Das würde das Verhalten ändern.
Noah
: Achselzucken: - Das Laufzeitverhalten wird nicht geändert. Kompilierungsfehler ist kein Laufzeitverhalten . Das "Verhalten" der add-Methode ändert sich auf diese Weise.
Jason S
-2

Ein weiterer Grund sind Schnittstellen. Hier ist ein Beispiel, um es zu zeigen:

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 */
}
Patrick
quelle
Sie zeigen etwas, mit dem Sie durchkommen können, weil remove()es nicht kovariant ist. Die Frage ist jedoch, ob dies sollte erlaubt sein. ArrayList#remove()arbeitet als Wertgleichheit, nicht als Referenzgleichheit. Warum würden Sie erwarten, dass a Bgleich a ist A? In Ihrem Beispiel es kann sein, aber es ist eine seltsame Erwartung. Ich würde lieber sehen, dass Sie MyClasshier ein Argument liefern .
seh
-3

Weil es vorhandenen Code (vor Java5) beschädigen würde. z.B,

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

Nun könnten Sie sagen, dass der obige Code falsch ist, aber nehmen Sie an, dass o aus einer heterogenen Menge von Objekten stammt (dh Zeichenfolgen, Zahlen, Objekte usw. enthält). Sie möchten alle Übereinstimmungen entfernen, was legal war, da beim Entfernen die Nicht-Zeichenfolgen einfach ignoriert wurden, weil sie nicht gleich waren. Aber wenn Sie es entfernen lassen (String o), funktioniert das nicht mehr.

Noah
quelle
4
Wenn ich eine Liste <String> instanziiere, würde ich erwarten, dass ich nur List.remove (someString) aufrufen kann. Wenn ich die Abwärtskompatibilität unterstützen muss, würde ich eine unformatierte Liste - Liste <?> Verwenden, dann kann ich list.remove (someObject) aufrufen, nein?
Chris Mazzola
5
Wenn Sie "entfernen" durch "hinzufügen" ersetzen, ist dieser Code genauso kaputt wie in Java 5.
DJClayworth