ChatGPT解决这个技术问题 Extra ChatGPT

Why shouldn't Java enum literals be able to have generic type parameters?

Java enums are great. So are generics. Of course we all know the limitations of the latter because of type erasure. But there is one thing I don't understand, Why can't I create an enum like this:

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

This generic type parameter <T> in turn could then be useful in various places. Imagine a generic type parameter to a method:

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

Or even in the enum class itself:

public T convert(Object o);

More concrete example #1

Since the above example might seem too abstract for some, here's a more real-life example of why I want to do this. In this example I want to use

Enums, because then I can enumerate a finite set of property keys

Generics, because then I can have method-level type-safety for storing properties

public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

More concrete example #2

I have an enumeration of data types:

public interface DataType<T> {}

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

Each enum literal would obviously have additional properties based on the generic type <T>, while at the same time, being an enum (immutable, singleton, enumerable, etc. etc.)

Question:

Did no one think of this? Is this a compiler-related limitation? Considering the fact, that the keyword "enum" is implemented as syntactic sugar, representing generated code to the JVM, I don't understand this limitation.

Who can explain this to me? Before you answer, consider this:

I know generic types are erased :-)

I know there are workarounds using Class objects. They're workarounds.

Generic types result in compiler-generated type casts wherever applicable (e.g. when calling the convert() method

The generic type would be on the enum. Hence it is bound by each of the enum's literals. Hence the compiler would know, which type to apply when writing something like String string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);

The same applies to the generic type parameter in the T getvalue() method. The compiler can apply type casting when calling String string = someClass.getValue(LITERAL1)

I don't understand this limitation either. I came across this recently where my enum contained different "Comparable" types, and with generics, only Comparable types of the same types can be compared without warnings that need to be suppressed (even though at runtime the proper ones would be compared). I could have gotten rid of these warnings by using a type bound in the enum to specify which comparable type was supported, but instead I had to add the SuppressWarnings annotation - no way around it! Since compareTo does throw a class cast exception anyways, it's ok I guess, but still...
(+1) I'm just in the middle of trying to close a type safety gap in my project, stopped dead by this arbitrary limitation. Just consider this: turn the enum into a "typesafe enum" idiom we used before Java 1.5. Suddenly, you can have your enum members parameterized. That's probably what I'm going to do now.
@EdwinDalorzo: Updated the question with a concrete example from jOOQ, where this would have been immensely useful in the past.
@LukasEder I see your point now. Looks like a cool a new feature. Maybe you should suggest it in project coin mailing list I see other interesting proposals there on enums, but not one like yours.
Totally agree. Enum without generics are crippled. Your case #1 is also mine. If I need generic enum I give up JDK5 and implement it in plain old Java 1.4 style. This approach has also additional benefit: I'm not forced to have all constants in one class or even package. Thus, package-by-feature style is accomplished a lot better. This is perfect just for configuration-like "enums" - constants are spread in packages according to their logical meaning (and if I wish to see them all, I have type hieararchy shown).

L
Lukas Eder

This has been discussed as of JEP-301 Enhanced Enums, which was withdrawn, regrettably. The example given in the JEP is, which is precisely what I was looking for:

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

Unfortunately, the JEP was struggling with significant issues, which couldn't be resolved: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html


As of December 2018, there were some signs of life around JEP 301 again, but skimming that discussion makes it clear that the problems are still far from solved.
This JEP was withdrawn in September 2020: withdrawing comment
M
Martin Algesten

The answer is in the question:

because of type erasure

None of these two methods are possible, since the argument type is erased.

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

To realise those methods you could however construct your enum as:

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

    private Class<?> clazz;

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

    ...

}

Well, erasure takes place at compile-time. But the compiler can use generic type information for type-checks. And then "converts" the generic type to type casts. I'll rephrase the question
Not sure I entirely follow. Take public T convert(Object);. I'm guessing this method could for instance narrow a bunch of different types to <T>, which for instance is a String. The construction of the String object is runtime - nothing the compiler does. Hence you need to know the runtime type, such as String.class. Or am I missing something?
I think you're missing the fact that the generic type is on the enum (or its generated class). The only instances of the enum are its literals, which all provide a constant generic type binding. Hence there is no ambiguity about T in public T convert(Object);
Aha! Got it. You're cleverer than me :)
I guess it comes down to the class for a enum being evaluated in a similar way to everything else. In java, although things seem constant, they aren't. Consider public final static String FOO; with a static block static { FOO = "bar"; } - also constants are evaluated even when known at compile time.
T
Tom Hawtin - tackline

Because you can't. Seriously. That could be added to the language spec. It hasn't been. It would add some complexity. That benefit to cost means it isn't a high priority.

Update: Currently being added to the language under JEP 301: Enhanced Enums.


Could you elaborate on the complications ?
I have been thinking about this for a couple of days now, and I still see no complications.
u
user1944408

There are other methods in ENUM that wouldn't work. What would MyEnum.values() return?

What about MyEnum.valueOf(String name)?

For the valueOf if you think that compiler could make generic method like

public static MyEnum valueOf(String name);

in order to call it like MyEnum<String> myStringEnum = MyEnum.value("some string property"), that wouldn't work either. For example what if you call MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property") ? It is not possible to implement that method to work correctly, for example to throw exception or return null when you call it like MyEnum.<Int>value("some double property") because of type erasure.


Why wouldn't they work? They'd simply use wildcards...: MyEnum<?>[] values() and MyEnum<?> valueOf(...)
But then you could not do assignment like this MyEnum<Int> blabla = valueOf("some double property"); because the types are not compatible. Also you want in that case to get null because you want to return MyEnum<Int> which does not exists for double property name and you cannot make that method work properly because of erasure.
Also if you loop through values() you would need to use MyEnum which is not what you want usually because for example you cannot loop through only your Int properties. Also you would need to do a lot of casting which you want to avoid, I would suggest to create different enums for every type or make your own class with instances...
Well, I guess you cannot have both. Usually, I resort to my own "enum" implementations. It's just that Enum has a lot of other useful features, and this one would be optional and very useful as well...
n
nsfyn55

Frankly this seems like more of a solution in search of a problem than anything.

The entire purpose of the java enum is to model a enumeration of type instances that share similiar properties in a way that provides consistency and richness beyond that of comparable String or Integer representations.

Take an example of a text book enum. This is not very useful or consistent:

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

Why would I want my different planets to have different generic type conversions? What problem does it solve? Does it justify complicating the language semantics? If I do need this behavior is an enum the best tool to achieve it?

Additionally how would you manage complex conversions?

for Instance

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

Its easy enough with String Integer to supply the name or ordinal. But generics would allow you to supply any type. How would you manage the conversion to MyComplexClass? Now your mucking up two constructs by forcing the compiler to know that there are a limited subset of types that can be supplied to generic enums and introducing additional confusion to concept(Generics) that already seems elude a lot of programmers.


Thinking of a couple of examples where it would not be useful is a terrible argument that it would never be useful.
The examples support the point. The enum instances are subclasses of the type(nice and simple) and that including generics is a can of worms it terms of complexity for a very obscure benefit. If you are going to downvote me then you should also downvote Tom Hawtin below who said the same thing in not so many words
@nsfyn55 Enums are one of the most complex, most magical language features of Java. There isn't a single other language feature which auto-generates static methods, for example. Plus, each enum type already is an instance of a generic type.
@nsfyn55 The design goal was to make enum members powerful and flexible, which is why they support such advanced features as custom instance methods and even mutable instance variables. They are designed to have behavior specialized for each member individually so they can participate as active collaborators in complex usage scenarios. Most of all, the Java enum was designed to make the general enumeration idiom type safe, but the lack of type parameters unfortunately makes it fall short of that most cherished goal. I am quite convinced that there was a very specific reason for that.
I didn't notice your mention of contravariance... Java does support it, only it's use-site, which makes it a bit unwieldy. 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); } We have to manually work out each time which type is consumed and which produced, and specify the bounds accordingly.
C
Christophe Moine

By using this Java annotation processor https://github.com/cmoine/generic-enums, you can write something like (the convert method were implemented as an example):

import org.cmoine.genericEnums.GenericEnum;
import org.cmoine.genericEnums.GenericEnumParam;

@GenericEnum
public enum MyEnum {
    LITERAL1(String.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o.toString(); // example
        }
    },
    LITERAL2(Integer.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o.hashCode(); // example
        }
    },
    LITERAL3(Object.class) {
        @Override
        @GenericEnumParam
        public Object convert(Object o) {
            return o; // example
        }
    };

    MyEnum(Class<?> clazz) {
    }

    @GenericEnumParam
    public abstract Object convert(Object o);
}

The annotation processor will generate an enum MyEnumExt (customizable) which overcomes the limitation of java enums. Instead, it generates a Java class usable exactly as an enum (in the end, an enum is compiled into a Java class implementing Enum !).


I
Ingo

Becasue "enum" is the abbreviation for enumeration. It's just a set of named constants that stand in place of ordinal numbers to make the code better readable.

I don't see what the intended meaning of a type-parameterized constant could be.


In java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. You see now? :-)
You said you don't see what the meaning of a type-parameterised constant could be. So I showed you a type-parameterised constant (not a function pointer).
Of course not, as the java language does not know such a thing as a function pointer. Still, not the constant is type parameterized, but it's type. And the type parameter itself is constant, not a type variable.
But the enum syntax is just syntactic sugar. Underneath, they're exactly like CASE_INSENSITIVE_ORDER... If Comparator<T> were an enum, why not have literals with any associated bound for <T>?
If Comparator were an enum, then indeed we could have all sorts of weird things. Perhaps something like Integer. But it isn't.
c
capsula

I think because basically Enums can't be instanced

Where would you set the T class, if JVM allowed you to do so?

Enumeration is data that is supposed to be always the same, or at least, that it won't change dinamically.

new MyEnum<>()?

Still the following approach may be useful

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

Not sure I like the setO() method on an enum. I consider enums constants and to me that implies immutable. So even if it's possible, I wouldn't do it.
Enums are instanciated in the code generated by the compiler. Each literal is actually created by calling the private constructors. Hence, there would be a chance to pass the generic type to the constructor at compile-time.
Setters on enums are completely normal. Especially if you're using the enum to create a singleton (Item 3 Effective Java by Joshua Bloch)