Warum behaupten einige, dass Javas Implementierung von Generika schlecht ist?

115

Ich habe gelegentlich gehört, dass Java mit Generika nicht richtig verstanden hat. (nächste Referenz hier )

Verzeihen Sie meine Unerfahrenheit, aber was hätte sie besser gemacht?

sdellysse
quelle
23
Ich sehe keinen Grund, dies zu schließen. Bei all dem Mist, der mit Generika zu tun hat, könnte eine Klärung der Unterschiede zwischen Java und C # Menschen helfen, die versuchen, dem schlecht informierten Schlammschlachten auszuweichen.
Rob

Antworten:

146

Schlecht:

  • Typinformationen gehen beim Kompilieren verloren, sodass Sie zur Ausführungszeit nicht sagen können, um welchen Typ es sich handelt
  • Kann nicht für Werttypen verwendet werden (dies ist ein großes Problem - in .NET wird a List<byte>wirklich durch ein byte[]Beispiel unterstützt, und es ist kein Boxen erforderlich).
  • Syntax zum Aufrufen generischer Methoden saugt (IMO)
  • Die Syntax für Einschränkungen kann verwirrend werden
  • Wildcarding ist im Allgemeinen verwirrend
  • Verschiedene Einschränkungen aufgrund der oben genannten - Casting etc.

Gut:

  • Mit Wildcarding kann die Kovarianz / Kontravarianz auf der Anruferseite angegeben werden, was in vielen Situationen sehr ordentlich ist
  • Es ist besser als nichts!
Jon Skeet
quelle
51
@Paul: Ich denke, ich hätte eigentlich einen vorübergehenden Bruch vorgezogen, aber danach eine anständige API. Oder nehmen Sie die .NET-Route und führen Sie neue Sammlungstypen ein. (.NET Generika in 2.0 haben 1.1 Apps nicht kaputt gemacht.)
Jon Skeet
6
Mit einer großen vorhandenen Codebasis gefällt mir die Tatsache, dass ich die Signatur einer Methode ändern kann, um Generika zu verwenden, ohne jeden zu ändern, der sie aufruft.
Paul Tomblin
38
Aber die Nachteile davon sind riesig und dauerhaft. Es gibt einen großen kurzfristigen Gewinn und einen langfristigen Verlust IMO.
Jon Skeet
11
Jon - "Es ist besser als nichts" ist subjektiv :)
MetroidFan2002
6
@ Rogerio: Du hast behauptet, mein erster "schlechter" Gegenstand sei nicht korrekt. Mein erstes "Bad" -Element besagt, dass Informationen beim Kompilieren verloren gehen. Damit dies nicht korrekt ist, müssen Sie sagen, dass Informationen beim Kompilieren nicht verloren gehen ... Was die Verwirrung anderer Entwickler betrifft, scheinen Sie der einzige zu sein, der durch das, was ich sage, verwirrt ist.
Jon Skeet
26

Das größte Problem ist, dass Java-Generika nur zur Kompilierungszeit verfügbar sind und Sie sie zur Laufzeit untergraben können. C # wird gelobt, weil es mehr Laufzeitprüfungen durchführt. In diesem Beitrag gibt es einige wirklich gute Diskussionen , die auf andere Diskussionen verweisen.

Paul Tomblin
quelle
Das ist nicht wirklich ein Problem, es wurde nie für die Laufzeit gemacht. Es ist, als ob Sie sagen, dass Boote ein Problem sind, weil sie keine Berge besteigen können. Java macht als Sprache das, was viele Menschen brauchen: Typen auf sinnvolle Weise ausdrücken. Für Verbesserungen vom Typ Laufzeit können Personen weiterhin ClassObjekte weitergeben.
Vincent Cantin
1
Er hat recht. Ihre Anologie ist falsch, weil die Leute, die das Boot entwerfen, es entworfen haben sollten, um Berge zu besteigen. Ihre Designziele waren nicht sehr gut durchdacht für das, was benötigt wurde. Jetzt stecken wir alle nur noch mit einem Boot fest und können keine Berge mehr besteigen.
WiegleyJ
1
@ VincentCantin - es ist definitiv ein Problem. Daher beschweren wir uns alle darüber. Java-Generika sind halbgebacken.
Josh M.
17

Das Hauptproblem ist, dass Java zur Laufzeit keine Generika hat. Es ist eine Funktion zur Kompilierungszeit.

Wenn Sie eine generische Klasse in Java erstellen, verwenden sie eine Methode namens "Type Erasure", um tatsächlich alle generischen Typen aus der Klasse zu entfernen und sie im Wesentlichen durch Object zu ersetzen. Die meilenhohe Version von Generika besteht darin, dass der Compiler einfach Casts in den angegebenen generischen Typ einfügt, wenn er im Methodenkörper angezeigt wird.

Dies hat viele Nachteile. Einer der größten, IMHO, ist, dass Sie keine Reflexion verwenden können, um einen generischen Typ zu untersuchen. Typen sind im Bytecode nicht generisch und können daher nicht als generische Typen überprüft werden.

Großartiger Überblick über die Unterschiede hier: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html

JaredPar
quelle
2
Ich bin mir nicht sicher, warum Sie herabgestimmt wurden. Soweit ich weiß, haben Sie Recht, und dieser Link ist sehr informativ. Up-Voted.
Jason Jackson
@ Jason, danke. In meiner Originalversion gab es einen Tippfehler, ich glaube, ich wurde dafür abgelehnt.
JaredPar
3
Err, da Sie können Reflexion Blick auf einen generischen Typ, eine generische Methode Signatur verwenden. Sie können Reflection einfach nicht verwenden, um die allgemeinen Informationen zu überprüfen, die einer parametrisierten Instanz zugeordnet sind. Wenn ich zum Beispiel eine Klasse mit einer Methode foo (List <String>) habe, kann ich "String"
oxbow_lakes
Es gibt viele Artikel, die besagen, dass das Löschen von Typen das Auffinden der tatsächlichen Typparameter unmöglich macht. Sagen wir: Objekt x = neues X [Y] (); Können Sie zeigen, wie Sie mit x den Typ Y finden können?
user48956
@oxbow_lakes - Reflektion verwenden zu müssen, um einen generischen Typ zu finden, ist fast so schlimm, als wenn man das nicht kann. ZB ist es eine schlechte Lösung.
Josh M.
14
  1. Laufzeitimplementierung (dh keine Typlöschung);
  2. Die Fähigkeit, primitive Typen zu verwenden (dies hängt mit (1) zusammen);
  3. Während das Platzhalterzeichen nützlich ist, ist die Syntax und das Wissen, wann es verwendet werden soll, etwas, das viele Leute verblüfft. und
  4. Keine Leistungsverbesserung (aufgrund von (1); Java-Generika sind syntaktischer Zucker für Castingi-Objekte).

(1) führt zu einem sehr seltsamen Verhalten. Das beste Beispiel, an das ich denken kann, ist. Annehmen:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

deklarieren Sie dann zwei Variablen:

MyClass<T> m1 = ...
MyClass m2 = ...

Rufen Sie jetzt an getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

Das zweite generische Typargument wird vom Compiler entfernt, da es sich um einen Rohtyp handelt (dh der parametrisierte Typ wird nicht angegeben), obwohl er nichts enthält mit dem parametrisierten Typ zu tun hat.

Ich werde auch meine Lieblingserklärung des JDK erwähnen:

public class Enum<T extends Enum<T>>

Abgesehen von Wildcarding (was eine gemischte Tasche ist) denke ich nur, dass die .Net-Generika besser sind.

Cletus
quelle
Aber der Compiler wird es Ihnen sagen, insbesondere mit der Option rawtypes lint.
Tom Hawtin - Tackline
Entschuldigung, warum ist die letzte Zeile ein Kompilierungsfehler? Ich spiele in Eclipse damit herum und kann es dort nicht zum Scheitern bringen - wenn ich genug Material hinzufüge, damit der Rest kompiliert werden kann.
oconnor0
public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop
@ oconnor0 Es ist kein Kompilierungsfehler, sondern eine Compiler-Warnung ohne ersichtlichen Grund:The expression of type List needs unchecked conversion to conform to List<String>
Artbristol
Enum<T extends Enum<T>>mag auf den ersten Blick seltsam / redundant erscheinen, ist aber tatsächlich ziemlich interessant, zumindest im Rahmen der Einschränkungen von Java / seinen Generika. Aufzählungen haben eine statische values()Methode, die ein Array ihrer Elemente angibt, die als Aufzählung und nicht als Aufzählung eingegeben werden. Dieser EnumTyp wird durch den generischen Parameter bestimmt, was bedeutet, dass Sie möchten Enum<T>. Natürlich ist diese Eingabe nur im Kontext eines Aufzählungstyps sinnvoll, und alle Aufzählungen sind Unterklassen von Enum, also möchten Sie Enum<T extends Enum>. Java mag es jedoch nicht, Rohtypen mit Generika zu mischen, Enum<T extends Enum<T>>um die Konsistenz zu gewährleisten.
JAB
10

Ich werde eine wirklich kontroverse Meinung abgeben. Generika erschweren die Sprache und den Code. Angenommen, ich habe eine Zuordnung, die eine Zeichenfolge einer Liste von Zeichenfolgen zuordnet. Früher konnte ich das einfach so erklären

Map someMap;

Jetzt muss ich es als deklarieren

Map<String, List<String>> someMap;

Und jedes Mal, wenn ich es in eine Methode übergebe, muss ich diese große lange Erklärung noch einmal wiederholen. Meiner Meinung nach lenkt all diese zusätzliche Eingabe den Entwickler ab und bringt ihn aus der "Zone". Wenn der Code mit viel Cruft gefüllt ist, ist es manchmal schwierig, später darauf zurückzukommen und schnell alle Cruft zu durchsuchen, um die wichtige Logik zu finden.

Java hat bereits einen schlechten Ruf als eine der ausführlichsten Sprachen, die allgemein verwendet werden, und Generika tragen nur zu diesem Problem bei.

Und was kaufen Sie wirklich für all diese zusätzliche Ausführlichkeit? Wie oft hatten Sie wirklich Probleme, wenn jemand eine Ganzzahl in eine Sammlung eingefügt hat, die Strings enthalten soll, oder wenn jemand versucht hat, einen String aus einer Sammlung von Ganzzahlen herauszuziehen? In meiner 10-jährigen Erfahrung beim Erstellen kommerzieller Java-Anwendungen war dies nie eine große Fehlerquelle. Ich bin mir also nicht sicher, was Sie für die zusätzliche Ausführlichkeit bekommen. Es kommt mir wirklich nur als extra bürokratisches Gepäck vor.

Jetzt werde ich wirklich kontrovers. Was ich als das größte Problem bei Sammlungen in Java 1.4 sehe, ist die Notwendigkeit, überall typisiert zu werden. Ich betrachte diese Typografien als zusätzliche, ausführliche Cruft, die viele der gleichen Probleme haben wie Generika. So kann ich zum Beispiel nicht einfach machen

List someList = someMap.get("some key");

Ich muss tun

List someList = (List) someMap.get("some key");

Der Grund ist natürlich, dass get () ein Objekt zurückgibt, das ein Supertyp von List ist. Die Zuordnung kann also nicht ohne Typografie erfolgen. Denken Sie noch einmal darüber nach, wie viel Ihnen diese Regel wirklich einbringt. Aus meiner Erfahrung nicht viel.

Ich denke, Java wäre viel besser dran gewesen, wenn 1) es keine Generika hinzugefügt hätte, sondern 2) stattdessen implizites Casting von einem Supertyp zu einem Subtyp erlaubt hätte. Lassen Sie zur Laufzeit falsche Casts abfangen. Dann hätte ich die Einfachheit des Definierens haben können

Map someMap;

und später tun

List someList = someMap.get("some key");

Die ganze Kruft wäre verschwunden, und ich glaube wirklich nicht, dass ich eine große neue Fehlerquelle in meinen Code einführen würde.

Clint Miller
quelle
15
Entschuldigung, ich bin mit fast allem, was Sie gesagt haben, nicht einverstanden. Aber ich werde es nicht ablehnen, weil Sie es gut argumentieren.
Paul Tomblin
2
Natürlich gibt es viele Beispiele für große, erfolgreiche Systeme, die in Sprachen wie Python und Ruby erstellt wurden und genau das tun, was ich in meiner Antwort vorgeschlagen habe.
Clint Miller
Ich denke, mein ganzer Einwand gegen diese Art von Idee ist nicht, dass es per se eine schlechte Idee ist, sondern dass moderne Sprachen wie Python und Ruby das Leben für Entwickler immer einfacher machen, Entwickler wiederum intellektuell selbstgefällig werden und schließlich eine niedrigere haben Verständnis des eigenen Codes.
Dan Tao
4
"Und was kaufen Sie wirklich für all diese zusätzliche Ausführlichkeit? ..." Es sagt dem armen Wartungsprogrammierer, was in diesen Sammlungen sein soll. Ich habe festgestellt, dass dies ein Problem bei der Arbeit mit kommerziellen Java-Anwendungen ist.
Richj
4
"Wie oft hattest du wirklich Probleme, wenn jemand ein [x] in eine Sammlung einfügt, die [y] enthalten soll?" - Oh Mann, ich habe die Zählung verloren! Auch wenn es keinen Fehler gibt, ist es ein Killer für die Lesbarkeit. Und das Scannen vieler Dateien, um herauszufinden, was das Objekt sein wird (oder das Debuggen), zieht mich wirklich aus der Zone heraus. Vielleicht magst du Haskell - sogar starkes Tippen, aber weniger Cruft (weil Typen abgeleitet werden).
Martin Capodici
8

Ein weiterer Nebeneffekt der Kompilierungszeit und nicht der Laufzeit ist, dass Sie den Konstruktor des generischen Typs nicht aufrufen können. Sie können sie also nicht verwenden, um eine generische Factory zu implementieren ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++

jdkoftinoff
quelle
6

Java - Generika sind für Korrektheit bei der Kompilierung überprüft und dann alle Typinformationen entfernt (der Vorgang wird als Typ Löschung . So generic List<Integer>wird auf seinen reduziert werden rohe Art , nicht-genericList , die Objekte beliebiger Klasse enthalten.

Dies führt dazu, dass zur Laufzeit beliebige Objekte in die Liste eingefügt werden können und es jetzt unmöglich ist zu sagen, welche Typen als generische Parameter verwendet wurden. Letzteres führt wiederum zu

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");
Anton Gogolev
quelle
5

Ich wünschte, dies wäre ein Wiki, damit ich es anderen Leuten hinzufügen könnte ... aber ...

Probleme:

  • Typ Löschen (keine Laufzeitverfügbarkeit)
  • Keine Unterstützung für primative Typen
  • Inkompatibilität mit Anmerkungen (beide wurden in Version 1.5 hinzugefügt. Ich bin mir immer noch nicht sicher, warum Anmerkungen keine Generika zulassen, außer die Funktionen zu beschleunigen.)
  • Inkompatibilität mit Arrays. (Manchmal möchte ich wirklich so etwas wie Class machen <? Erweitert MyObject >[], aber ich darf nicht)
  • Seltsame Wildcard-Syntax und Verhalten
  • Die Tatsache, dass die generische Unterstützung für alle Java-Klassen inkonsistent ist. Sie haben es den meisten Sammlungsmethoden hinzugefügt, aber hin und wieder stößt man auf eine Instanz, in der es nicht vorhanden ist.
Laplie Anderson
quelle
5

Generika, wie angegeben, funktionieren einfach nicht.

Dies kompiliert:

List<Integer> x = Collections.emptyList();

Dies ist jedoch ein Syntaxfehler:

foo(Collections.emptyList());

Wo foo definiert ist als:

void foo(List<Integer> x) { /* method body not important */ }

Ob ein Ausdruckstyp prüft, hängt also davon ab, ob er einer lokalen Variablen oder einem tatsächlichen Parameter eines Methodenaufrufs zugewiesen wird. Wie verrückt ist das?

Nat
quelle
3
Die inkonsistente Folgerung durch Javac ist Mist.
mP.
3
Ich würde annehmen, dass der Grund dafür, dass das letztere Formular abgelehnt wird, darin besteht, dass es aufgrund von Methodenüberladung möglicherweise mehr als eine Version von "foo" gibt.
Jason Creighton
2
Diese Kritik gilt nicht mehr, da Java 8 eine verbesserte Inferenz vom
Zieltyp
4

Die Einführung von Generika in Java war eine schwierige Aufgabe, da die Architekten versuchten, Funktionalität, Benutzerfreundlichkeit und Abwärtskompatibilität mit Legacy-Code in Einklang zu bringen. Erwartungsgemäß mussten Kompromisse eingegangen werden.

Es gibt einige, die auch der Meinung sind, dass die Implementierung von Generika durch Java die Komplexität der Sprache auf ein inakzeptables Maß erhöht hat (siehe Ken Arnolds " Generics Considered Harmful "). Angelika Langers FAQs zu Generika geben eine ziemlich gute Vorstellung davon, wie kompliziert Dinge werden können.

Zach Scrivena
quelle
3

Java erzwingt Generics nicht zur Laufzeit, sondern nur zur Kompilierungszeit.

Dies bedeutet, dass Sie interessante Dinge tun können, z. B. generische Sammlungen mit den falschen Typen versehen.

Powerlord
quelle
1
Der Compiler wird Ihnen sagen, dass Sie es vermasselt haben, wenn Sie das schaffen.
Tom Hawtin - Tackline
@ Tom - nicht unbedingt. Es gibt Möglichkeiten, den Compiler zu täuschen.
Paul Tomblin
2
Hören Sie nicht zu, was der Compiler Ihnen sagt? (siehe meinen ersten Kommentar)
Tom Hawtin - Tackline
1
@ Tom: Wenn Sie über eine ArrayList <String> verfügen und diese an eine Methode alten Stils (z. B. Legacy- oder Code von Drittanbietern) übergeben, die eine einfache Liste verwendet, kann diese Methode dieser Liste beliebige Elemente hinzufügen. Wenn es zur Laufzeit erzwungen wird, erhalten Sie eine Ausnahme.
Paul Tomblin
1
statische Leere addToList (Listenliste) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (Liste); Das wird kompiliert und funktioniert sogar zur Laufzeit. Das einzige Problem, das Sie sehen, ist, wenn Sie aus der Liste entfernen und eine Zeichenfolge erwarten und ein int erhalten.
Laplie Anderson
3

Java-Generika sind nur zur Kompilierungszeit und werden in nicht generischen Code kompiliert. In C # ist die tatsächlich kompilierte MSIL generisch. Dies hat enorme Auswirkungen auf die Leistung, da Java zur Laufzeit noch ausgeführt wird. Weitere Informationen finden Sie hier .

Szymon Rozga
quelle
2

Wenn Sie Java Posse # 279 - Interview mit Joe Darcy und Alex Buckley hören , sprechen sie über dieses Problem. Das verweist auch auf einen Neal Gafter-Blogbeitrag mit dem Titel Reified Generics for Java , der besagt:

Viele Menschen sind mit den Einschränkungen, die durch die Implementierung von Generika in Java verursacht werden, nicht zufrieden. Insbesondere sind sie unglücklich darüber, dass generische Typparameter nicht geändert werden: Sie sind zur Laufzeit nicht verfügbar. Generika werden durch Löschen implementiert, wobei generische Typparameter einfach zur Laufzeit entfernt werden.

Dieser Blog-Beitrag verweist auf einen älteren Eintrag, Puzzling Through Erasure: Antwortabschnitt , in dem der Punkt zur Migrationskompatibilität in den Anforderungen hervorgehoben wurde.

Ziel war es, die Abwärtskompatibilität von Quell- und Objektcode sowie die Migrationskompatibilität zu gewährleisten.

Kevin Hakanson
quelle