Dlaczego literały wyliczenia Java nie powinny mieć ogólnych parametrów typu?


148

Wyliczenia Java są świetne. Więc są generyczne. Oczywiście wszyscy znamy ograniczenia tego ostatniego z powodu wymazywania typów. Ale jest jedna rzecz, której nie rozumiem, dlaczego nie mogę utworzyć takiego wyliczenia:

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

Ten parametr typu ogólnego <T>może z kolei być przydatny w różnych miejscach. Wyobraź sobie ogólny parametr typu dla metody:

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

Lub nawet w samej klasie enum:

public T convert(Object o);

Bardziej konkretny przykład nr 1

Ponieważ powyższy przykład może wydawać się zbyt abstrakcyjny dla niektórych, oto bardziej prawdziwy przykład, dlaczego chcę to zrobić. W tym przykładzie chcę użyć

  • Wyliczenia, ponieważ wtedy mogę wyliczyć skończony zestaw kluczy właściwości
  • Generics, ponieważ wtedy mogę mieć bezpieczeństwo typu na poziomie metody do przechowywania właściwości
public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

Bardziej konkretny przykład nr 2

Mam wyliczenie typów danych:

public interface DataType<T> {}

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

Każdy literał wyliczenia miałby oczywiście dodatkowe właściwości oparte na typie ogólnym <T> , będąc jednocześnie wyliczeniem (niezmiennym, pojedynczym, wyliczalnym itp.)

Pytanie:

Czy nikt o tym nie pomyślał? Czy to jest ograniczenie związane z kompilatorem? Biorąc pod uwagę fakt, że słowo kluczowe „ enum ” jest zaimplementowane jako cukier składniowy, reprezentujący wygenerowany kod w JVM, nie rozumiem tego ograniczenia.

Kto może mi to wyjaśnić? Zanim odpowiesz, zastanów się:

  • Wiem, że typy ogólne są usuwane :-)
  • Wiem, że istnieją obejścia przy użyciu obiektów Class. To obejścia.
  • Typy ogólne skutkują rzutami typów generowanych przez kompilator wszędzie tam, gdzie ma to zastosowanie (np. Podczas wywoływania metody convert ()
  • Typ ogólny <T> znajdowałby się w wyliczeniu. Stąd jest on związany przez każdy z literałów wyliczenia. Dlatego kompilator wiedziałby, jaki typ zastosować podczas pisania czegoś takiegoString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • To samo dotyczy parametru typu ogólnego w T getvalue()metodzie. Kompilator może zastosować rzutowanie typu podczas wywoływaniaString string = someClass.getValue(LITERAL1)

3
Nie rozumiem też tego ograniczenia. Niedawno natknąłem się na to, gdy moje wyliczenie zawierało różne typy „porównywalne”, aw przypadku typów ogólnych tylko porównywalne typy tego samego typu mogą być porównywane bez ostrzeżeń, które muszą być wyłączone (nawet jeśli w czasie wykonywania porównywano by te właściwe). Mogłem pozbyć się tych ostrzeżeń, używając typu związanego w wyliczeniu, aby określić, który porównywalny typ był obsługiwany, ale zamiast tego musiałem dodać adnotację SuppressWarnings - nie ma sposobu na obejście tego! Ponieważ compareTo i tak rzuca wyjątek rzucania klas, myślę, że jest ok, ale nadal ...
MetroidFan2002

5
(+1) Jestem właśnie w trakcie próby wypełnienia luki bezpieczeństwa typu w moim projekcie, zatrzymana przez to arbitralne ograniczenie. Po prostu zastanów się: enumzamień idiom „wyliczenie bezpieczne dla typów”, którego używaliśmy przed Javą 1.5. Nagle możesz sparametryzować członków wyliczenia. To prawdopodobnie to, co teraz zrobię.
Marko Topolnik

1
@EdwinDalorzo: Zaktualizowano pytanie , dodając konkretny przykład z jOOQ , gdzie w przeszłości byłoby to niezwykle przydatne.
Lukas Eder

2
@LukasEder Teraz rozumiem twój punkt widzenia. Wygląda na fajną nową funkcję. Może powinieneś zasugerować to na liście mailingowej projektu coin. Widzę tam inne ciekawe propozycje na wyliczeniach, ale nie taką jak twoja.
Edwin Dalorzo

1
Kompletnie się zgadzam. Wyliczenie bez typów ogólnych jest okaleczone. Twoja sprawa nr 1 jest również moja. Jeśli potrzebuję ogólnego wyliczenia, rezygnuję z JDK5 i implementuję go w zwykłym, starym stylu Java 1.4. Takie podejście ma również dodatkową zaletę : nie jestem zmuszony mieć wszystkich stałych w jednej klasie lub nawet pakiecie. W ten sposób styl pakiet po funkcji jest znacznie lepszy. Jest to idealne tylko dla „wyliczeń” przypominających konfigurację - stałe są rozmieszczane w pakietach zgodnie z ich logicznym znaczeniem (a jeśli chcę je wszystkie zobaczyć, pokazana jest hierarchia typów).
Tomáš Záluský

Odpowiedzi:


50

Jest to obecnie omawiane jako ulepszone wyliczenia JEP-301 . Przykład podany w JEP to dokładnie to, czego szukałem:

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

Niestety JEP wciąż boryka się z istotnymi problemami: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html


2
W grudniu 2018 r. Ponownie pojawiły się oznaki życia wokół JEP 301 , ale przeglądanie tej dyskusji jasno pokazuje, że problemy wciąż są dalekie od rozwiązania.
Pont

11

Odpowiedź jest w pytaniu:

z powodu usunięcia typu

Żadna z tych dwóch metod nie jest możliwa, ponieważ typ argumentu jest usuwany.

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

Aby zrealizować te metody, możesz jednak skonstruować wyliczenie jako:

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

    private Class<?> clazz;

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

    ...

}

2
Cóż, wymazywanie odbywa się w czasie kompilacji. Ale kompilator może używać informacji o typie ogólnym do sprawdzania typów. Następnie „konwertuje” typ ogólny na rzutowanie typów. Przeformułuję pytanie
Lukas Eder

2
Nie jestem pewien, czy całkowicie to rozumiem. Weź public T convert(Object);. Domyślam się, że ta metoda może na przykład zawęzić kilka różnych typów do <T>, na przykład typu String. Konstrukcja obiektu String to runtime - nic nie robi kompilator. Dlatego musisz znać typ środowiska uruchomieniowego, taki jak String.class. A może coś mi brakuje?
Martin Algesten

6
Myślę, że brakuje ci faktu, że typ ogólny <T> znajduje się w wyliczeniu (lub w jego wygenerowanej klasie). Jedynymi wystąpieniami wyliczenia są jego literały, które zapewniają stałe powiązanie typu ogólnego. Dlatego nie ma dwuznaczności co do T w publicznej konwersji T (obiekt);
Lukas Eder

2
Aha! Rozumiem. Jesteś mądrzejszy ode mnie :)
Martin Algesten

1
Myślę, że sprowadza się to do klasy, w której wyliczenie jest oceniane w podobny sposób, jak wszystko inne. W Javie, chociaż rzeczy wydają się stałe, tak nie jest. Rozważ public final static String FOO;w przypadku bloku statycznego static { FOO = "bar"; }- również stałe są oceniane, nawet jeśli są znane w czasie kompilacji.
Martin Algesten

5

Ponieważ nie możesz. Poważnie. Można to dodać do specyfikacji języka. Nie było. Dodałoby to trochę złożoności. Ta korzyść kosztowa oznacza, że ​​nie jest to wysoki priorytet.

Aktualizacja: obecnie dodawane do języka pod JEP 301: Rozszerzone wyliczenia .


Czy mógłbyś opisać komplikacje?
Mr_and_Mrs_D

4
Myślałem o tym od kilku dni i nadal nie widzę żadnych komplikacji.
Marko Topolnik

4

Istnieją inne metody w ENUM, które nie zadziałają. Co by MyEnum.values()zwróciło?

O co chodzi MyEnum.valueOf(String name)?

Dla valueOf, jeśli uważasz, że kompilator może utworzyć metodę ogólną, taką jak

public static MyEnum valueOf (nazwa ciągu);

żeby to nazwać jak MyEnum<String> myStringEnum = MyEnum.value("some string property") , to też nie zadziała. Na przykład co jeśli zadzwonisz MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")? Nie jest możliwe zaimplementowanie tej metody, aby działała poprawnie, na przykład aby zgłosić wyjątek lub zwrócić wartość null, gdy wywołujesz ją tak, jak z MyEnum.<Int>value("some double property")powodu wymazywania typu.


2
Dlaczego nie mieliby działać? Użyliby po prostu symboli wieloznacznych ...: MyEnum<?>[] values()iMyEnum<?> valueOf(...)
Lukas Eder,

1
Ale wtedy nie możesz wykonać takiego przypisania, MyEnum<Int> blabla = valueOf("some double property");ponieważ typy nie są kompatybilne. Również w tym przypadku chcesz uzyskać wartość null, ponieważ chcesz zwrócić MyEnum <Int>, która nie istnieje dla podwójnej nazwy właściwości i nie możesz sprawić, by ta metoda działała poprawnie z powodu wymazania.
user1944408

Ponadto, jeśli zapętlasz przez wartości (), będziesz musiał użyć MyEnum <?>, Co zwykle nie jest tym, czego potrzebujesz, ponieważ na przykład nie możesz zapętlić tylko swoich właściwości Int. Ponadto musiałbyś wykonać dużo rzutowania, którego chcesz uniknąć, sugerowałbym utworzenie różnych wyliczeń dla każdego typu lub stworzenie własnej klasy z instancjami ...
user1944408

Cóż, myślę, że nie możesz mieć obu. Zwykle korzystam z własnych implementacji typu „enum”. Po prostu Enumma wiele innych przydatnych funkcji, a ta byłaby opcjonalna i bardzo przydatna ...
Lukas Eder

0

Szczerze mówiąc, wydaje się to bardziej rozwiązaniem w poszukiwaniu problemu niż czegokolwiek.

Całym celem wyliczenia Java jest modelowanie wyliczenia wystąpień typów, które mają podobne właściwości w sposób zapewniający spójność i bogactwo wykraczające poza porównywalne reprezentacje typu String lub Integer.

Weź przykład enum z podręcznika. To nie jest zbyt przydatne ani spójne:

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

Dlaczego miałbym chcieć, aby moje różne planety miały różne konwersje typów ogólnych? Jaki problem rozwiązuje? Czy uzasadnia to komplikowanie semantyki języka? Jeśli potrzebuję tego zachowania, czy wyliczenie jest najlepszym narzędziem do osiągnięcia tego?

Poza tym, jak zarządzasz złożonymi konwersjami?

na przykład

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

Wystarczy String Integerpodać nazwę lub numer porządkowy. Ale leki generyczne pozwoliłyby na dostarczenie dowolnego typu. Jak poradzisz sobie z konwersją do MyComplexClass? Teraz zbierasz dwie konstrukcje, zmuszając kompilator, aby wiedział, że istnieje ograniczony podzbiór typów, które można dostarczyć do ogólnych wyliczeń i wprowadzając dodatkowe zamieszanie w koncepcji (Generics), która wydaje się wymykać się wielu programistom.


17
Wymyślenie kilku przykładów, w których nie byłoby to przydatne, jest okropnym argumentem, że nigdy nie byłoby przydatne.
Elias Vasylenko

1
Przykłady potwierdzają ten punkt. Instancje wyliczeniowe są podklasami typu (ładne i proste), a uwzględnienie typów ogólnych jest puszką robaków, których złożoność jest bardzo złożona, co daje bardzo niejasne korzyści. Jeśli masz zamiar zagłosować w dół, powinieneś również zagłosować poniżej Toma Hawtina, który powiedział to samo w kilku słowach
nsfyn55.

1
@ nsfyn55 Enum to jedna z najbardziej złożonych i magicznych funkcji języka Java. Na przykład nie ma żadnej innej funkcji językowej, która automatycznie generuje metody statyczne. Ponadto każdy typ wyliczenia jest już wystąpieniem typu ogólnego.
Marko Topolnik

1
@ nsfyn55 Celem projektu było stworzenie potężnych i elastycznych elementów wyliczeniowych, dlatego obsługują one tak zaawansowane funkcje, jak niestandardowe metody instancji, a nawet zmienne instancji z możliwością mutacji. Są zaprojektowane tak, aby zachowywać się indywidualnie dla każdego członka, aby mogli uczestniczyć jako aktywni współpracownicy w złożonych scenariuszach użytkowania. Przede wszystkim wyliczenie w Javie zostało zaprojektowane w celu zapewnienia bezpieczeństwa typu ogólnego idiomu wyliczania , ale brak parametrów typu niestety sprawia, że ​​nie spełnia tego najbardziej cenionego celu. Jestem przekonany, że był ku temu bardzo konkretny powód.
Marko Topolnik

1
Nie zauważyłem twojej wzmianki o kontrawariancji ... Java to obsługuje, tylko że jest witryną użycia, co sprawia, że ​​jest trochę nieporęczna. 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); }Za każdym razem musimy ręcznie określić, który typ jest używany, a który wyprodukowany, i odpowiednio określić ograniczenia.
Marko Topolnik

-2

Becasue „enum” to skrót od wyliczenia. To tylko zbiór nazwanych stałych, które zastępują liczby porządkowe, aby kod był bardziej czytelny.

Nie rozumiem, jakie może być zamierzone znaczenie stałej sparametryzowanej na typ.


1
W java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. Teraz widzisz? :-)
Lukas Eder

4
Powiedziałeś, że nie rozumiesz, jakie może być znaczenie stałej sparametryzowanej na typ. Więc pokazałem ci stałą sparametryzowaną przez typ (nie wskaźnik funkcji).
Lukas Eder

Oczywiście, że nie, ponieważ język java nie zna czegoś takiego jak wskaźnik funkcji. Jednak nie stała jest sparametryzowana na typ, ale jest to typ. Sam parametr typu jest stały, a nie zmienną typu.
Ingo

Ale składnia enum to po prostu cukier składniowy. Pod spodem są dokładnie takie, jak CASE_INSENSITIVE_ORDER... Gdyby Comparator<T>wyliczenie było, dlaczego nie mieć literałów z powiązanymi ograniczeniami dla <T>?
Lukas Eder

Gdyby komparator <T> był wyliczeniem, to rzeczywiście moglibyśmy mieć różne dziwne rzeczy. Być może coś takiego jak Integer <T>. Ale tak nie jest.
Ingo

-3

Myślę, ponieważ w zasadzie wyliczenia nie mogą być instancjami

Gdzie ustawiłbyś klasę T, gdyby JVM pozwolił ci to zrobić?

Wyliczenie to dane, które mają być zawsze takie same lub przynajmniej takie, że nie zmieniają się dynamicznie.

nowy MyEnum <> ()?

Wciąż przydatne może być poniższe podejście

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

8
Nie jestem pewien, czy podoba mi się setO()metoda na enum. Uważam, że stałe wyliczenia są stałe i dla mnie oznacza to niezmienne . Więc nawet jeśli to możliwe, nie zrobiłbym tego.
Martin Algesten

2
Wyliczenia są umieszczane w kodzie generowanym przez kompilator. Każdy literał jest faktycznie tworzony przez wywołanie prywatnych konstruktorów. W związku z tym byłaby szansa na przekazanie typu ogólnego do konstruktora w czasie kompilacji.
Lukas Eder

2
Setery na wyliczeniach są całkowicie normalne. Zwłaszcza jeśli używasz wyliczenia do tworzenia singletona (Item 3 Effective Java, Joshua Bloch)
Preston
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.