Warum sollte es mich interessieren, dass Java keine reifizierten Generika hat?


102

Dies war eine Frage, die ich kürzlich in einem Interview gestellt hatte, als etwas, das der Kandidat der Java-Sprache hinzufügen wollte. Es wird allgemein als Schmerz bezeichnet, dass Java keine generischen Generika hat, aber wenn der Kandidat dazu gedrängt wurde, konnte er mir nicht sagen, was er hätte erreichen können, wenn sie dort gewesen wären.

Da in Java Rohtypen zulässig sind (und unsichere Überprüfungen), ist es offensichtlich möglich, Generika zu untergraben und am Ende eine zu erhalten List<Integer>, die (zum Beispiel) tatsächlich Strings enthält . Dies könnte eindeutig unmöglich gemacht werden, wenn die Typinformationen geändert würden. aber es muss noch mehr geben !

Könnten die Leute Beispiele für Dinge posten, die sie wirklich tun möchten, wenn reifizierte Generika verfügbar sind? Ich meine, natürlich könnten Sie zur ListLaufzeit den Typ eines bekommen - aber was würden Sie damit machen?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

BEARBEITEN : Eine schnelle Aktualisierung, da die Antworten hauptsächlich über die Notwendigkeit besorgt zu sein scheinen, einen Classals Parameter zu übergeben (zum Beispiel EnumSet.noneOf(TimeUnit.class)). Ich habe mehr nach etwas gesucht, bei dem dies einfach nicht möglich ist . Beispielsweise:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?

Würde dies bedeuten, dass Sie zur Laufzeit die Klasse eines generischen Typs erhalten könnten? (Wenn ja, habe ich ein Beispiel!)
James B

Ich denke, der größte Teil des Wunsches nach reifizierbaren Generika kommt von Menschen, die Generika hauptsächlich für Sammlungen verwenden und möchten, dass sich diese Sammlungen eher wie Arrays verhalten.
kdgregory

2
Die interessantere Frage (für mich): Was würde es brauchen, um Generika im C ++ - Stil in Java zu implementieren? Es scheint zur Laufzeit sicherlich machbar zu sein, würde aber alle vorhandenen Klassenladeprogramme beschädigen (da findClass()die Parametrisierung ignoriert werden müsste, dies aber defineClass()nicht konnte). Und wie wir wissen, stehen die Powers That Be für die Abwärtskompatibilität an erster Stelle.
kdgregory

1
Tatsächlich bietet Java reified Generics auf sehr eingeschränkte Weise an . Ich biete
Richard Gomes

Die JavaOne-Keynote gibt an, dass Java 9 die Reifizierung unterstützt.
Rangi Keen

Antworten:


81

Von den wenigen Malen, in denen ich auf dieses "Bedürfnis" gestoßen bin, läuft es letztendlich auf dieses Konstrukt hinaus:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Dies funktioniert in C # unter der Annahme, dass Tein Standardkonstruktor vorhanden ist . Sie können sogar den Laufzeit-Typ von typeof(T)und die Konstruktoren von abrufen Type.GetConstructor().

Die übliche Java-Lösung wäre, das Class<T>Argument as zu übergeben.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(Es muss nicht unbedingt als Konstruktorargument übergeben werden, da ein Methodenargument ebenfalls in Ordnung ist. Das Obige ist nur ein Beispiel, auch das try-catchwird der Kürze halber weggelassen.)

Für alle anderen generischen Typkonstrukte kann der tatsächliche Typ mit ein wenig Reflexion leicht aufgelöst werden. Die folgenden Fragen und Antworten veranschaulichen die Anwendungsfälle und Möglichkeiten:


5
Dies funktioniert (zum Beispiel) in C #? Woher wissen Sie, dass T einen Standardkonstruktor hat?
Thomas Jung

4
Ja es tun. Und dies ist nur ein einfaches Beispiel (nehmen wir an, wir arbeiten mit Javabohnen). Der springende Punkt ist, dass Sie mit Java die Klasse zur Laufzeit nicht von T.classoder abrufen könnenT.getClass() , sodass Sie auf alle Felder, Konstruktoren und Methoden zugreifen können. Es macht auch das Bauen unmöglich.
BalusC

3
Dies scheint mir als "großes Problem" sehr schwach zu sein, zumal dies wahrscheinlich nur in Verbindung mit einer sehr spröden Reflexion über Konstruktoren / Parameter usw.
nützlich ist

33
Es wird in C # kompiliert, vorausgesetzt, Sie deklarieren den Typ als: public class Foo <T> wobei T: new (). Dadurch werden die gültigen T-Typen auf diejenigen beschränkt, die einen parameterlosen Konstruktor enthalten.
Martin Harris

3
@Colin Sie können Java tatsächlich mitteilen, dass ein bestimmter generischer Typ erforderlich ist, um mehr als eine Klasse oder Schnittstelle zu erweitern. Weitere Informationen finden Sie im Abschnitt "Mehrere Grenzen" von docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Natürlich können Sie nicht sagen, dass das Objekt zu einer bestimmten Gruppe gehört, die keine Methoden gemeinsam nutzt, die in eine Schnittstelle abstrahiert werden könnten, die mithilfe der Vorlagenspezialisierung in C ++ etwas implementiert werden könnte.)
JAB

99

Das, was mich am häufigsten beißt, ist die Unfähigkeit, den Mehrfachversand über mehrere generische Typen hinweg zu nutzen. Folgendes ist nicht möglich und es gibt viele Fälle, in denen dies die beste Lösung wäre:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }

5
Es besteht absolut keine Notwendigkeit einer Verdinglichung, um dies zu tun. Die Methodenauswahl erfolgt zur Kompilierungszeit, wenn die Informationen zum Typ der Kompilierungszeit verfügbar sind.
Tom Hawtin - Tackline

26
@ Tom: Dies wird nicht einmal kompiliert, da der Typ gelöscht wird. Beide werden kompiliert als public void my_method(List input) {}. Ich bin jedoch nie auf dieses Bedürfnis gestoßen, einfach weil sie nicht den gleichen Namen haben würden. Wenn sie den gleichen Namen haben, würde ich fragen, ob public <T extends Object> void my_method(List<T> input) {}das keine bessere Idee ist.
BalusC

1
Hm, ich würde dazu neigen, eine Überladung mit identischer Anzahl von Parametern insgesamt zu vermeiden und so etwas wie myStringsMethod(List<String> input)und myIntegersMethod(List<Integer> input)selbst dann zu bevorzugen, wenn eine Überladung für einen solchen Fall in Java möglich wäre.
Fabian Steeg

4
@Fabian: Das heißt, Sie müssen separaten Code haben und die Vorteile vermeiden, die Sie <algorithm>in C ++ erhalten.
David Thornley

Ich bin verwirrt, das ist im Wesentlichen das Gleiche wie mein zweiter Punkt, aber ich habe 2 Downvotes dazu? Möchte mich jemand aufklären?
rsp

34

Typensicherheit fällt mir ein. Das Downcasting auf einen parametrisierten Typ ist ohne reifizierte Generika immer unsicher:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Außerdem würden Abstraktionen weniger auslaufen - zumindest diejenigen, die an Laufzeitinformationen zu ihren Typparametern interessiert sein könnten. Wenn Sie heute Laufzeitinformationen über den Typ eines der generischen Parameter benötigen, müssen Sie diese ebenfalls weitergeben Class. Auf diese Weise hängt Ihre externe Schnittstelle von Ihrer Implementierung ab (ob Sie RTTI für Ihre Parameter verwenden oder nicht).


1
Ja - ich kann das umgehen, indem ich eine erstelle, ParametrizedListdie die Daten in den Prüftypen der Quellensammlung kopiert. Es ist ein bisschen wie Collections.checkedList, kann aber zunächst mit einer Sammlung besiedelt werden.
oxbow_lakes

@tackline - nun, ein paar Abstraktionen würden weniger auslaufen. Wenn Sie in Ihrer Implementierung Zugriff auf Typmetadaten benötigen, werden Sie von der externen Schnittstelle darüber informiert, da Clients Ihnen ein Klassenobjekt senden müssen.
Gustafc

... was bedeutet, dass Sie mit reifizierten Generika Dinge wie T.class.getAnnotation(MyAnnotation.class)(wo Tist ein generischer Typ) hinzufügen können, ohne die externe Schnittstelle zu ändern.
Gustafc

@gustafc: Wenn Sie der Meinung sind, dass C ++ - Vorlagen Ihnen vollständige Typensicherheit
bieten

@kdgregory: Ich habe nie gesagt, dass C ++ 100% typsicher ist - nur dass das Löschen die Typensicherheit beschädigt. Wie Sie selbst sagen: "C ++ hat, wie sich herausstellt, eine eigene Form der Typlöschung, die als Zeigerumwandlung im C-Stil bekannt ist." Java führt jedoch nur dynamische Casts durch (keine Neuinterpretation), sodass eine Reifizierung das Ganze in das Typsystem einbinden würde.
Gustafc

26

Sie können generische Arrays in Ihrem Code erstellen.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}

Was ist los mit Object? Ein Array von Objekten ist sowieso ein Array von Referenzen. Es ist nicht so, dass die Objektdaten auf dem Stapel liegen - es ist alles auf dem Haufen.
Ran Biron

19
Typensicherheit. Ich kann in Object [] alles einfügen, was ich will, aber nur Strings in String [].
Turnor

3
Ran: Ohne sarkastisch zu sein: Sie möchten vielleicht eine Skriptsprache anstelle von Java verwenden, dann haben Sie die Flexibilität, untypisierte Variablen überall zu verwenden!
fliegende Schafe

2
Arrays sind in beiden Sprachen kovariant (und daher nicht typsicher). String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Kompiliert gut in beiden Sprachen. Läuft notsofine.
Martijn

16

Dies ist eine alte Frage, es gibt eine Menge Antworten, aber ich denke, dass die vorhandenen Antworten falsch sind.

"reifiziert" bedeutet nur real und bedeutet normalerweise nur das Gegenteil von Typlöschung.

Das große Problem im Zusammenhang mit Java Generics:

  • Diese schreckliche Boxanforderung und Trennung zwischen Grundelementen und Referenztypen. Dies hängt nicht direkt mit der Verdinglichung oder dem Löschen von Typen zusammen. C # / Scala beheben dies.
  • Keine Selbsttypen. JavaFX 8 musste aus diesem Grund "Builder" entfernen. Absolut nichts mit Typlöschung zu tun. Scala behebt das Problem und ist sich bei C # nicht sicher.
  • Keine deklarationsseitige Typabweichung. C # 4.0 / Scala haben dies. Absolut nichts mit Typlöschung zu tun.
  • Kann nicht überladen void method(List<A> l)und method(List<B> l). Dies ist auf das Löschen des Typs zurückzuführen, aber äußerst kleinlich.
  • Keine Unterstützung für die Laufzeitreflexion. Dies ist das Herzstück der Typlöschung. Wenn Sie hochentwickelte Compiler mögen, die zum Zeitpunkt der Kompilierung so viel wie möglich von Ihrer Programmlogik überprüfen und beweisen, sollten Sie so wenig wie möglich Reflektion verwenden, und diese Art der Löschung sollte Sie nicht stören. Wenn Sie mehr lückenhafte, scripty, dynamische Typprogrammierung mögen und sich nicht so sehr dafür interessieren, dass ein Compiler so viel wie möglich von Ihrer Logik korrekt beweist, dann ist es wichtig, dass Sie besser reflektieren und das Löschen des Typs korrigieren.

1
Meistens fällt es mir bei Serialisierungsfällen schwer. Sie möchten oft in der Lage sein, die Klassentypen von generischen Dingen zu ermitteln, die serialisiert werden, aber Sie werden aufgrund der Typlöschung angehalten. Es macht es schwer, so etwas zu tundeserialize(thingy, List<Integer>.class)
Cogman

3
Ich denke, das ist die beste Antwort. Insbesondere Teile, die beschreiben, welche Probleme aufgrund der Typlöschung wirklich grundlegend sind und was nur Probleme beim Entwurf der Java-Sprache sind. Das erste, was ich den Leuten erzähle, die mit dem Löschen beginnen, ist, dass Scala und Haskell auch nach Art der Löschung arbeiten.
Aemxdp

13

Die Serialisierung wäre mit der Reifizierung einfacher. Was wir wollen würden, ist

deserialize(thingy, List<Integer>.class);

Was wir tun müssen, ist

deserialize(thing, new TypeReference<List<Integer>>(){});

sieht hässlich aus und funktioniert funkig.

Es gibt auch Fälle, in denen es sehr hilfreich wäre, so etwas zu sagen

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Diese Dinge beißen nicht oft, aber sie beißen, wenn sie passieren.


Dies. Tausendmal so. Jedes Mal, wenn ich versuche, Deserialisierungscode in Java zu schreiben, beklage ich den Tag, dass ich nicht in etwas anderem arbeite
Basic

11

Meine Exposition gegenüber Java Geneircs ist sehr begrenzt, und abgesehen von den Punkten, die andere Antworten bereits erwähnt haben, gibt es ein Szenario, das in dem Buch Java Generics and Collections von Maurice Naftalin und Philip Walder erläutert wird, in dem die reifizierten Generika nützlich sind.

Da die Typen nicht überprüfbar sind, können keine parametrisierten Ausnahmen festgelegt werden.

Zum Beispiel ist die Erklärung des folgenden Formulars ungültig.

class ParametericException<T> extends Exception // compile error

Dies liegt daran, dass die catch- Klausel prüft, ob die ausgelöste Ausnahme einem bestimmten Typ entspricht. Diese Prüfung entspricht der Prüfung durch den Instanztest. Da der Typ nicht überprüfbar ist, ist die obige Form der Anweisung ungültig.

Wenn der obige Code gültig wäre, wäre eine Ausnahmebehandlung auf die folgende Weise möglich gewesen:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

Das Buch erwähnt auch, dass wenn Java-Generika ähnlich wie C ++ - Vorlagen definiert werden (Erweiterung), dies zu einer effizienteren Implementierung führen kann, da dies mehr Möglichkeiten zur Optimierung bietet. Bietet aber keine Erklärung mehr als diese, so dass jede Erklärung (Hinweise) von den sachkundigen Leuten hilfreich wäre.


1
Es ist ein gültiger Punkt, aber ich bin mir nicht ganz sicher, warum eine so parametrisierte Ausnahmeklasse nützlich wäre. Könnten Sie Ihre Antwort so ändern, dass sie ein kurzes Beispiel dafür enthält, wann dies nützlich sein könnte?
oxbow_lakes

@oxbow_lakes: Entschuldigung. Meine Kenntnisse über Java Generics sind sehr begrenzt und ich versuche, sie zu verbessern. Daher kann ich mir kein Beispiel vorstellen, bei dem eine parametrisierte Ausnahme nützlich sein könnte. Ich werde versuchen, darüber nachzudenken. Vielen Dank.
Sateesh

Es könnte als Ersatz für die Mehrfachvererbung von Ausnahmetypen dienen.
Meriton

Die Leistungsverbesserung besteht darin, dass derzeit ein Typparameter von Object erben muss, was das Boxen primitiver Typen erfordert, was einen Ausführungs- und Speicheraufwand verursacht.
Meriton

8

Arrays würden mit Generika wahrscheinlich viel besser spielen, wenn sie reifiziert würden.


2
Sicher, aber sie hätten immer noch Probleme. List<String>ist nicht ein List<Object>.
Tom Hawtin - Tackline

Zustimmen - aber nur für Grundelemente (Integer, Long usw.). Für "reguläres" Objekt ist dies dasselbe. Da Primitive kein parametrisierter Typ sein können (zumindest meiner Meinung nach ein weitaus schwerwiegenderes Problem), sehe ich dies nicht als echten Schmerz an.
Ran Biron

3
Das Problem bei Arrays ist ihre Kovarianz, die nichts mit Reifizierung zu tun hat.
Recurse

5

Ich habe einen Wrapper, der eine JDBC-Ergebnismenge als Iterator darstellt (dies bedeutet, dass ich von einer Datenbank stammende Operationen durch Abhängigkeitsinjektion viel einfacher testen kann).

Die API sieht so aus, als ob Iterator<T>T ein Typ ist, der nur mit Zeichenfolgen im Konstruktor erstellt werden kann. Der Iterator überprüft dann die von der SQL-Abfrage zurückgegebenen Zeichenfolgen und versucht, sie einem Konstruktor vom Typ T zuzuordnen.

Bei der aktuellen Implementierung von Generika muss ich auch die Klasse der Objekte übergeben, die ich aus meiner Ergebnismenge erstellen werde. Wenn ich richtig verstehe, wenn Generika reifiziert würden, könnte ich einfach T.getClass () aufrufen, um seine Konstruktoren zu erhalten, und dann nicht das Ergebnis von Class.newInstance () umwandeln müssen, was weitaus ordentlicher wäre.

Grundsätzlich denke ich, dass es das Schreiben von APIs (im Gegensatz zum Schreiben einer Anwendung) einfacher macht, da Sie viel mehr aus Objekten ableiten können und daher weniger Konfiguration erforderlich ist ... Ich habe die Auswirkungen von Anmerkungen erst zu schätzen gewusst, als ich sah, dass sie in Dingen wie spring oder xstream anstelle von Unmengen von Konfigurationen verwendet wurden.


Aber in der Klasse zu bestehen scheint mir rundum sicherer. In jedem Fall ist das reflektierte Erstellen von Instanzen aus Datenbankabfragen für Änderungen wie das Refactoring (sowohl in Ihrem Code als auch in der Datenbank) äußerst spröde. Ich glaube, ich habe nach Dingen gesucht, bei denen es einfach nicht möglich ist , die Klasse bereitzustellen
oxbow_lakes

5

Eine schöne Sache wäre, das Boxen für primitive (Wert-) Typen zu vermeiden. Dies hängt etwas mit der Array-Beschwerde zusammen, die andere erhoben haben, und in Fällen, in denen die Speichernutzung eingeschränkt ist, kann dies tatsächlich einen signifikanten Unterschied bewirken.

Es gibt auch verschiedene Arten von Problemen beim Schreiben eines Frameworks, bei denen es wichtig ist, über den parametrisierten Typ nachdenken zu können. Natürlich kann dies umgangen werden, indem ein Klassenobjekt zur Laufzeit übergeben wird. Dies verdeckt jedoch die API und belastet den Benutzer des Frameworks zusätzlich.


2
Dies new T[]läuft im Wesentlichen darauf hinaus, in der Lage zu sein, dort zu tun, wo T vom primitiven Typ ist!
oxbow_lakes

2

Es ist nicht so, dass Sie etwas Außergewöhnliches erreichen würden. Es wird einfach einfacher zu verstehen sein. Das Löschen von Texten scheint für Anfänger eine schwierige Zeit zu sein, und es erfordert letztendlich Verständnis für die Funktionsweise des Compilers.

Meiner Meinung nach sind Generika einfach ein Extra , das viel redundantes Casting spart.


Dies ist sicherlich, was Generika in Java sind . In C # und anderen Sprachen sind sie ein mächtiges Werkzeug
Basic

1

Etwas, das alle Antworten hier übersehen haben und das mir ständig Kopfschmerzen bereitet, ist, dass Sie eine generische Schnittstelle nicht zweimal erben können, da die Typen gelöscht werden. Dies kann ein Problem sein, wenn Sie feinkörnige Schnittstellen erstellen möchten.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!

0

Hier ist eine, die mich heute erwischt hat: Wenn Sie eine Methode schreiben, die eine varargs Liste generischer Elemente akzeptiert, können Anrufer ohne Bestätigung denken, dass sie typsicher sind, aber versehentlich einen alten Rohstoff übergeben und Ihre Methode in die Luft jagen.

Scheint unwahrscheinlich, dass das passieren würde? ... Sicher, bis ... Sie Class als Datentyp verwenden. Zu diesem Zeitpunkt sendet Ihnen Ihr Anrufer gerne viele Klassenobjekte, aber ein einfacher Tippfehler sendet Ihnen Klassenobjekte, die sich nicht an T halten, und Katastrophenfälle.

(NB: Ich habe hier vielleicht einen Fehler gemacht, aber beim Googeln um "Generika-Varargs" scheint das oben Genannte genau das zu sein, was Sie erwarten würden. Das, was dies zu einem praktischen Problem macht, ist die Verwendung von Klasse, denke ich - Anrufer scheinen weniger vorsichtig sein :()


Zum Beispiel verwende ich ein Paradigma, das Klassenobjekte als Schlüssel in Karten verwendet (es ist komplexer als eine einfache Karte - aber konzeptionell ist das der Fall).

zB funktioniert dies hervorragend in Java Generics (triviales Beispiel):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

zB ohne Reifizierung in Java Generics akzeptiert dieses JEDES "Class" -Objekt. Und es ist nur eine winzige Erweiterung des vorherigen Codes:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Die oben genannten Methoden müssen in einem einzelnen Projekt tausende Male ausgeschrieben werden - damit die Wahrscheinlichkeit menschlicher Fehler hoch wird. Das Debuggen von Fehlern erweist sich als "nicht lustig". Ich versuche gerade, eine Alternative zu finden, habe aber nicht viel Hoffnung.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.