Warum sollten Java-Enum-Literale keine generischen Typparameter haben können?

148

Java-Enums sind großartig. Generika auch. Natürlich kennen wir alle die Einschränkungen des letzteren aufgrund der Typlöschung. Aber eines verstehe ich nicht: Warum kann ich so eine Aufzählung nicht erstellen:

public enum MyEnum<T> {
    LITERAL1<String>,
    LITERAL2<Integer>,
    LITERAL3<Object>;
}

Dieser generische Typparameter <T>könnte dann wiederum an verschiedenen Stellen nützlich sein. Stellen Sie sich einen generischen Typparameter für eine Methode vor:

public <T> T getValue(MyEnum<T> param);

Oder sogar in der Enum-Klasse selbst:

public T convert(Object o);

Konkreteres Beispiel Nr. 1

Da das obige Beispiel für manche zu abstrakt erscheint, ist hier ein realistischeres Beispiel dafür, warum ich dies tun möchte. In diesem Beispiel möchte ich verwenden

  • Aufzählungen, weil ich dann einen endlichen Satz von Eigenschaftsschlüsseln aufzählen kann
  • Generika, weil ich dann Typensicherheit auf Methodenebene zum Speichern von Eigenschaften haben kann
public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

Konkreteres Beispiel Nr. 2

Ich habe eine Aufzählung von Datentypen:

public interface DataType<T> {}

public enum SQLDataType<T> implements DataType<T> {
    TINYINT<Byte>,
    SMALLINT<Short>,
    INT<Integer>,
    BIGINT<Long>,
    CLOB<String>,
    VARCHAR<String>,
    ...
}

Jedes Aufzählungsliteral hätte offensichtlich zusätzliche Eigenschaften, die auf dem generischen Typ basieren <T>, während es gleichzeitig eine Aufzählung ist (unveränderlich, singleton, aufzählbar usw. usw.).

Frage:

Hat niemand daran gedacht? Ist dies eine compilerbezogene Einschränkung? In Anbetracht der Tatsache, dass das Schlüsselwort " enum " als syntaktischer Zucker implementiert ist und generierten Code für die JVM darstellt, verstehe ich diese Einschränkung nicht.

Wer kann mir das erklären? Beachten Sie Folgendes, bevor Sie antworten:

  • Ich weiß, dass generische Typen gelöscht werden :-)
  • Ich weiß, dass es Problemumgehungen mit Klassenobjekten gibt. Sie sind Problemumgehungen.
  • Generische Typen führen gegebenenfalls zu vom Compiler generierten Typumwandlungen (z. B. beim Aufrufen der convert () -Methode
  • Der generische Typ <T> würde in der Aufzählung stehen. Daher ist es an jedes Literal der Aufzählung gebunden. Daher würde der Compiler wissen, welchen Typ er beim Schreiben von so etwas anwenden sollString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • Gleiches gilt für den generischen Typparameter in der T getvalue()Methode. Der Compiler kann beim Aufruf Type Casting anwendenString string = someClass.getValue(LITERAL1)
Lukas Eder
quelle
3
Ich verstehe diese Einschränkung auch nicht. Ich bin kürzlich darauf gestoßen, dass meine Aufzählung verschiedene "vergleichbare" Typen enthielt, und mit Generika können nur vergleichbare Typen desselben Typs ohne Warnungen verglichen werden, die unterdrückt werden müssen (obwohl zur Laufzeit die richtigen verglichen würden). Ich hätte diese Warnungen beseitigen können, indem ich einen in der Aufzählung gebundenen Typ verwendet hätte, um anzugeben, welcher vergleichbare Typ unterstützt wurde, aber stattdessen musste ich die Annotation SuppressWarnings hinzufügen - kein Weg daran vorbei! Da compareTo sowieso eine Klassenbesetzungsausnahme auslöst, ist es in Ordnung, denke ich, aber immer noch ...
MetroidFan2002
5
(+1) Ich bin gerade dabei, eine Sicherheitslücke in meinem Projekt zu schließen, die durch diese willkürliche Einschränkung gestoppt wurde. Bedenken Sie enumFolgendes : Verwandeln Sie das in eine "typsichere Aufzählung", die wir vor Java 1.5 verwendet haben. Plötzlich können Sie Ihre Enum-Mitglieder parametrisieren lassen. Das werde ich jetzt wahrscheinlich tun.
Marko Topolnik
1
@EdwinDalorzo: Die Frage wurde mit einem konkreten Beispiel von jOOQ aktualisiert , wo dies in der Vergangenheit immens nützlich gewesen wäre.
Lukas Eder
2
@ LukasEder Ich verstehe deinen Standpunkt jetzt. Sieht nach einer coolen neuen Funktion aus. Vielleicht sollten Sie es in der Projektmünz-Mailingliste vorschlagen. Ich sehe dort andere interessante Vorschläge in Aufzählungen, aber keine wie Ihre.
Edwin Dalorzo
1
Stimme voll und ganz zu. Enum ohne Generika sind verkrüppelt. Ihr Fall Nr. 1 gehört auch mir. Wenn ich eine generische Aufzählung benötige, gebe ich JDK5 auf und implementiere es im einfachen alten Java 1.4-Stil. Dieser Ansatz hat auch einen zusätzlichen Vorteil : Ich bin nicht gezwungen, alle Konstanten in einer Klasse oder sogar einem Paket zu haben. Somit wird der Paket-für-Merkmal-Stil viel besser erreicht. Dies ist nur für konfigurationsähnliche "Aufzählungen" perfekt - Konstanten werden entsprechend ihrer logischen Bedeutung in Paketen verteilt (und wenn ich sie alle sehen möchte, wird Typ Hieararchy angezeigt).
Tomáš Záluský

Antworten:

50

Dies wird jetzt ab JEP-301 Enhanced Enums diskutiert . Das im JEP gegebene Beispiel ist genau das, wonach ich gesucht habe:

enum Argument<X> { // declares generic enum
   STRING<String>(String.class), 
   INTEGER<Integer>(Integer.class), ... ;

   Class<X> clazz;

   Argument(Class<X> clazz) { this.clazz = clazz; }

   Class<X> getClazz() { return clazz; }
}

Class<String> cs = Argument.STRING.getClazz(); //uses sharper typing of enum constant

Leider hat das JEP immer noch mit erheblichen Problemen zu kämpfen: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html

Lukas Eder
quelle
2
Ab Dezember 2018 gab es wieder einige Lebenszeichen um JEP 301 , aber das Überfliegen dieser Diskussion macht deutlich, dass die Probleme noch lange nicht gelöst sind.
Pont
11

Die Antwort liegt in der Frage:

wegen Typlöschung

Keine dieser beiden Methoden ist möglich, da der Argumenttyp gelöscht wird.

public <T> T getValue(MyEnum<T> param);
public T convert(Object);

Um diese Methoden zu realisieren, können Sie Ihre Aufzählung jedoch wie folgt erstellen:

public enum MyEnum {
    LITERAL1(String.class),
    LITERAL2(Integer.class),
    LITERAL3(Object.class);

    private Class<?> clazz;

    private MyEnum(Class<?> clazz) {
      this.clazz = clazz;
    }

    ...

}
Martin Algesten
quelle
2
Nun, das Löschen erfolgt zur Kompilierungszeit. Der Compiler kann jedoch generische Typinformationen für Typprüfungen verwenden. Und dann "konvertiert" der generische Typ in Typ-Casts. Ich werde die Frage umformulieren
Lukas Eder
2
Ich bin mir nicht sicher, ob ich ganz folge. Nimm public T convert(Object);. Ich vermute, diese Methode könnte zum Beispiel eine Reihe verschiedener Typen auf <T> eingrenzen, was zum Beispiel ein String ist. Die Konstruktion des String-Objekts ist Laufzeit - nichts, was der Compiler tut. Daher müssen Sie den Laufzeittyp kennen, z. B. String.class. Oder fehlt mir etwas?
Martin Algesten
6
Ich denke, Sie vermissen die Tatsache, dass sich der generische Typ <T> in der Aufzählung (oder seiner generierten Klasse) befindet. Die einzigen Instanzen der Aufzählung sind ihre Literale, die alle eine konstante generische Typbindung bereitstellen. Daher gibt es keine Unklarheit über T in der öffentlichen T-Konvertierung (Objekt);
Lukas Eder
2
Aha! Verstanden. Du bist schlauer als ich :)
Martin Algesten
1
Ich denke, es kommt auf die Klasse an, dass eine Aufzählung auf ähnliche Weise wie alles andere bewertet wird. In Java scheinen die Dinge zwar konstant zu sein, aber nicht. Betrachten Sie public final static String FOO;mit einem statischen Block static { FOO = "bar"; }- auch Konstanten werden ausgewertet, selbst wenn sie zur Kompilierungszeit bekannt sind.
Martin Algesten
5

Weil du nicht kannst. Ernsthaft. Das könnte der Sprachspezifikation hinzugefügt werden. Das war es nicht. Dies würde die Komplexität erhöhen. Dieser Kostenvorteil bedeutet, dass es keine hohe Priorität hat.

Update: Wird derzeit zur Sprache unter JEP 301: Enhanced Enums hinzugefügt .

Tom Hawtin - Tackline
quelle
Könnten Sie die Komplikationen näher erläutern?
Mr_and_Mrs_D
4
Ich habe jetzt seit ein paar Tagen darüber nachgedacht und sehe immer noch keine Komplikationen.
Marko Topolnik
4

Es gibt andere Methoden in ENUM, die nicht funktionieren würden. Was würde MyEnum.values()zurückkehren?

Was ist mit MyEnum.valueOf(String name)?

Für den Wert von Wenn Sie denken, dass der Compiler generische Methoden wie machen könnte

public static MyEnum valueOf (String name);

um es so zu nennen MyEnum<String> myStringEnum = MyEnum.value("some string property"), würde das auch nicht funktionieren. Was ist zum Beispiel, wenn Sie anrufen MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")? Es ist nicht möglich, diese Methode so zu implementieren, dass sie ordnungsgemäß funktioniert, z. B. um eine Ausnahme auszulösen oder null zurückzugeben, wenn Sie sie MyEnum.<Int>value("some double property")aufgrund des Löschens des Typs wie folgt aufrufen .

user1944408
quelle
2
Warum würden sie nicht arbeiten? Sie würden einfach Wildcards verwenden ...: MyEnum<?>[] values()undMyEnum<?> valueOf(...)
Lukas Eder
1
Aber dann könnten Sie eine solche Zuordnung MyEnum<Int> blabla = valueOf("some double property");nicht vornehmen, da die Typen nicht kompatibel sind. Außerdem möchten Sie in diesem Fall null erhalten, weil Sie MyEnum <Int> zurückgeben möchten, das für den Namen der doppelten Eigenschaft nicht vorhanden ist, und diese Methode aufgrund des Löschvorgangs nicht ordnungsgemäß funktionieren kann.
user1944408
Auch wenn Sie values ​​() durchlaufen, müssen Sie MyEnum <?> Verwenden, was normalerweise nicht gewünscht wird, da Sie beispielsweise nicht nur Ihre Int-Eigenschaften durchlaufen können. Außerdem müssten Sie viel Casting durchführen, das Sie vermeiden möchten. Ich würde vorschlagen, für jeden Typ unterschiedliche Aufzählungen zu erstellen oder eine eigene Klasse mit Instanzen zu erstellen ...
user1944408
Nun, ich denke du kannst nicht beides haben. Normalerweise greife ich auf meine eigenen "Enum" -Implementierungen zurück. Es ist nur so, dass Enumes viele andere nützliche Funktionen gibt, und diese wäre optional und auch sehr nützlich ...
Lukas Eder
0

Ehrlich gesagt scheint dies eher eine Lösung auf der Suche nach einem Problem zu sein als alles andere.

Der gesamte Zweck der Java-Aufzählung besteht darin, eine Aufzählung von Typinstanzen zu modellieren, die ähnliche Eigenschaften aufweisen, und zwar auf eine Weise, die Konsistenz und Reichhaltigkeit bietet, die über die vergleichbarer String- oder Integer-Darstellungen hinausgeht.

Nehmen Sie ein Beispiel für eine Lehrbuchaufzählung. Dies ist nicht sehr nützlich oder konsistent:

public enum Planet<T>{
    Earth<Planet>,
    Venus<String>,
    Mars<Long>
    ...etc.
}

Warum sollte ich wollen, dass meine verschiedenen Planeten unterschiedliche generische Typkonvertierungen haben? Welches Problem löst es? Rechtfertigt es, die Sprachsemantik zu komplizieren? Wenn ich dieses Verhalten brauche, ist eine Aufzählung das beste Werkzeug, um es zu erreichen?

Wie würden Sie außerdem komplexe Conversions verwalten?

zum Beispiel

public enum BadIdea<T>{
   INSTANCE1<Long>,
   INSTANCE2<MyComplexClass>;
}

Es ist einfach genug String Integer, den Namen oder die Ordnungszahl anzugeben. Mit Generika können Sie jedoch jeden Typ liefern. Wie würden Sie die Konvertierung verwalten MyComplexClass? Jetzt machen Sie zwei Konstrukte kaputt, indem Sie den Compiler dazu zwingen, zu wissen, dass es eine begrenzte Teilmenge von Typen gibt, die generischen Enums zur Verfügung gestellt werden können, und zusätzliche Verwirrung in das Konzept (Generics) bringen, die sich bereits vielen Programmierern zu entziehen scheint.

nsfyn55
quelle
17
Ein paar Beispiele zu betrachten, bei denen es nicht nützlich wäre, ist ein schreckliches Argument dafür, dass es niemals nützlich sein würde.
Elias Vasylenko
1
Die Beispiele unterstützen den Punkt. Die Enum-Instanzen sind Unterklassen des Typs (nett und einfach), und das Einbeziehen von Generika ist eine Dose Würmer, deren Komplexität ein sehr unklarer Vorteil ist. Wenn Sie mich ablehnen wollen, sollten Sie auch Tom Hawtin unten ablehnen, der dasselbe in nicht so vielen Worten gesagt hat
nsfyn55
1
@ nsfyn55 Aufzählungen sind eine der komplexesten und magischsten Sprachfunktionen von Java. Es gibt keine einzige andere Sprachfunktion, die beispielsweise statische Methoden automatisch generiert. Plus, jeder Aufzählungstyp bereits ist eine Instanz eines generischen Typs.
Marko Topolnik
1
@ nsfyn55 Das Entwurfsziel bestand darin, Enum-Mitglieder leistungsfähig und flexibel zu machen, weshalb sie erweiterte Funktionen wie benutzerdefinierte Instanzmethoden und sogar veränderbare Instanzvariablen unterstützen. Sie sind so konzipiert, dass das Verhalten für jedes Mitglied individuell spezialisiert ist, damit sie als aktive Mitarbeiter an komplexen Nutzungsszenarien teilnehmen können. Vor allem wurde die Java-Enumeration entwickelt, um die allgemeine Aufzählungssprache sicher zu machen , aber das Fehlen von Typparametern führt leider dazu, dass dieses am meisten geschätzte Ziel nicht erreicht wird. Ich bin ziemlich davon überzeugt, dass es dafür einen ganz bestimmten Grund gab.
Marko Topolnik
1
Ich habe nicht bemerkt Ihre Erwähnung Kontra ... Java tut es unterstützen, nur die ist , die Nutzung vor Ort, die es ein bisschen unhandlich macht. interface Converter<IN, OUT> { OUT convert(IN in); } <E> Set<E> convertListToSet(List<E> in, Converter<? super List<E>, ? extends Set<E>> converter) { return converter.convert(in); }Wir müssen jedes Mal manuell herausfinden, welcher Typ verbraucht und welcher produziert wird, und die Grenzen entsprechend angeben.
Marko Topolnik
-2

Weil "Aufzählung" die Abkürzung für Aufzählung ist. Es ist nur eine Reihe benannter Konstanten, die anstelle von Ordnungszahlen stehen, um den Code besser lesbar zu machen.

Ich sehe nicht, was die beabsichtigte Bedeutung einer typparametrisierten Konstante sein könnte.

Ingo
quelle
1
In java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. Du siehst jetzt? :-)
Lukas Eder
4
Sie sagten, Sie sehen nicht, was die Bedeutung einer typparametrisierten Konstante sein könnte. Also habe ich Ihnen eine typparametrisierte Konstante gezeigt (kein Funktionszeiger).
Lukas Eder
Natürlich nicht, da die Java-Sprache so etwas wie einen Funktionszeiger nicht kennt. Dennoch ist nicht die Konstante typparametriert, sondern der Typ. Und der Typparameter selbst ist konstant und keine Typvariable.
Ingo
Die Enum-Syntax ist jedoch nur syntaktischer Zucker. Darunter sind sie genau wie CASE_INSENSITIVE_ORDER... Wenn Comparator<T>es sich um eine Aufzählung handelt, warum nicht Literale mit einer zugehörigen Bindung <T>?
Lukas Eder
Wenn Comparator <T> eine Aufzählung wäre, dann könnten wir tatsächlich alle möglichen seltsamen Dinge haben. Vielleicht so etwas wie Integer <T>. Aber es ist nicht so.
Ingo
-3

Ich denke, weil Enums im Grunde nicht instanziiert werden können

Wo würden Sie die T-Klasse einstellen, wenn JVM dies zulässt?

Aufzählung sind Daten, die immer gleich sein sollen oder sich zumindest nicht dinamisch ändern.

neues MyEnum <> ()?

Dennoch kann der folgende Ansatz nützlich sein

public enum MyEnum{

    LITERAL1("s"),
    LITERAL2("a"),
    LITERAL3(2);

    private Object o;

    private MyEnum(Object o) {
        this.o = o;
    }

    public Object getO() {
        return o;
    }

    public void setO(Object o) {
        this.o = o;
    }   
}
Kapsel
quelle
8
Ich bin mir nicht sicher, ob mir die setO()Methode auf einem gefällt enum. Ich betrachte Aufzählungskonstanten und für mich bedeutet das unveränderlich . Selbst wenn es möglich wäre, würde ich es nicht tun.
Martin Algesten
2
Aufzählungen werden in dem vom Compiler generierten Code instanziiert. Jedes Literal wird tatsächlich durch Aufrufen der privaten Konstruktoren erstellt. Daher besteht die Möglichkeit, den generischen Typ zur Kompilierungszeit an den Konstruktor zu übergeben.
Lukas Eder
2
Setter auf Enums sind völlig normal. Vor allem, wenn Sie die Aufzählung verwenden, um einen Singleton zu erstellen (Item 3 Effective Java von Joshua Bloch)
Preston