¿Por qué los literales Java enum no deberían tener parámetros de tipo genérico?

148

Las enumeraciones de Java son geniales. Así son los genéricos. Por supuesto, todos conocemos las limitaciones de este último debido al tipo de borrado. Pero hay una cosa que no entiendo, ¿por qué no puedo crear una enumeración como esta:

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

Este parámetro de tipo genérico <T>a su vez podría ser útil en varios lugares. Imagine un parámetro de tipo genérico para un método:

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

O incluso en la clase enum:

public T convert(Object o);

Ejemplo más concreto # 1

Dado que el ejemplo anterior puede parecer demasiado abstracto para algunos, aquí hay un ejemplo más real de por qué quiero hacer esto. En este ejemplo quiero usar

  • Enums, porque entonces puedo enumerar un conjunto finito de claves de propiedad
  • Genéricos, porque entonces puedo tener seguridad de tipos a nivel de método para almacenar propiedades
public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

Ejemplo más concreto # 2

Tengo una enumeración de tipos de datos:

public interface DataType<T> {}

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

Cada enum literal obviamente tendría propiedades adicionales basadas en el tipo genérico <T>, al mismo tiempo que sería una enumeración (inmutable, singleton, enumerable, etc., etc.)

Pregunta:

¿Nadie pensó en esto? ¿Es esta una limitación relacionada con el compilador? Teniendo en cuenta el hecho de que la palabra clave " enum " se implementa como azúcar sintáctico, que representa el código generado para la JVM, no entiendo esta limitación.

¿Quién me puede explicar esto? Antes de responder, considere esto:

  • Sé que los tipos genéricos se borran :-)
  • Sé que hay soluciones alternativas al usar objetos de clase. Son soluciones alternativas.
  • Los tipos genéricos resultan en conversiones de tipo generadas por el compilador cuando corresponda (por ejemplo, al llamar al método convert ()
  • El tipo genérico <T> estaría en la enumeración. Por lo tanto, está obligado por cada uno de los literales de la enumeración. Por lo tanto, el compilador sabría qué tipo aplicar al escribir algo comoString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • Lo mismo se aplica al parámetro de tipo genérico en el T getvalue()método. El compilador puede aplicar la conversión de tipos al llamarString string = someClass.getValue(LITERAL1)
Lukas Eder
fuente
3
Tampoco entiendo esta limitación. Me encontré con esto recientemente donde mi enumeración contenía diferentes tipos "Comparables", y con los genéricos, solo se pueden comparar tipos comparables de los mismos tipos sin advertencias que deben suprimirse (aunque en el tiempo de ejecución se compararían los adecuados). Podría haberme librado de estas advertencias utilizando un tipo enlazado en la enumeración para especificar qué tipo comparable era compatible, pero en lugar de eso tuve que agregar la anotación SuppressWarnings, ¡de ninguna manera! Dado que compareTo arroja una excepción de lanzamiento de clase de todos modos, está bien, supongo, pero aún así ...
MetroidFan2002
55
(+1) Estoy tratando de cerrar una brecha de seguridad tipo en mi proyecto, detenida por esta limitación arbitraria. Solo considere esto: enumconviértalo en un lenguaje de "enumeración segura de tipos" que usamos antes de Java 1.5. De repente, puede tener sus miembros de enumeración parametrizados. Eso es probablemente lo que voy a hacer ahora.
Marko Topolnik
1
@EdwinDalorzo: Actualicé la pregunta con un ejemplo concreto de jOOQ , donde esto habría sido inmensamente útil en el pasado.
Lukas Eder
2
@LukasEder Ahora veo tu punto. Parece genial una nueva característica. Tal vez deberías sugerirlo en la lista de correo de monedas del proyecto . Veo otras propuestas interesantes allí en enumeraciones, pero no una como la tuya.
Edwin Dalorzo
1
Totalmente de acuerdo. Enum sin genéricos están paralizados. Su caso # 1 también es mío. Si necesito una enumeración genérica, renuncio a JDK5 y lo implemento en el estilo antiguo y simple de Java 1.4. Este enfoque también tiene un beneficio adicional : no estoy obligado a tener todas las constantes en una clase o incluso paquete. Por lo tanto, el estilo de paquete por característica se logra mucho mejor. Esto es perfecto solo para "enumeraciones" de configuración: las constantes se distribuyen en paquetes de acuerdo con su significado lógico (y si deseo verlos a todos, se muestra la jerarquía de tipos).
Tomáš Záluský

Respuestas:

50

Esto ahora se está discutiendo a partir de las enumeraciones mejoradas JEP-301 . El ejemplo dado en el JEP es, que es precisamente lo que estaba buscando:

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

Desafortunadamente, el JEP todavía está luchando con problemas importantes: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html

Lukas Eder
fuente
2
A partir de diciembre de 2018, hubo algunas señales de vida alrededor del JEP 301 nuevamente , pero al descuidar esa discusión, queda claro que los problemas aún están lejos de resolverse.
Pont
11

La respuesta está en la pregunta:

por el tipo de borrado

Ninguno de estos dos métodos es posible, ya que el tipo de argumento se borra.

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

Sin embargo, para realizar esos métodos, podría construir su enumeración como:

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

    private Class<?> clazz;

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

    ...

}
Martin Algesten
fuente
2
Bueno, el borrado tiene lugar en tiempo de compilación. Pero el compilador puede usar información de tipo genérico para verificaciones de tipo. Y luego "convierte" el tipo genérico a tipos de conversión. Reformularé la pregunta
Lukas Eder
2
No estoy seguro de seguir por completo. Tomar public T convert(Object);. Supongo que este método podría, por ejemplo, limitar un montón de tipos diferentes a <T>, que por ejemplo es una Cadena. La construcción del objeto String es tiempo de ejecución, nada que haga el compilador. Por lo tanto, debe conocer el tipo de tiempo de ejecución, como String.class. ¿O me estoy perdiendo algo?
Martin Algesten
66
Creo que te estás perdiendo el hecho de que el tipo genérico <T> está en la enumeración (o su clase generada). Las únicas instancias de la enumeración son sus literales, que proporcionan un enlace de tipo genérico constante. Por lo tanto, no hay ambigüedad acerca de T en public T convert (Object);
Lukas Eder
2
¡Ajá! Entendido. Eres más listo que yo :)
Martin Algesten
1
Supongo que todo se reduce a la clase para una enumeración que se evalúa de manera similar a todo lo demás. En Java, aunque las cosas parecen constantes, no lo son. Considere public final static String FOO;con un bloque estático static { FOO = "bar"; }: también se evalúan las constantes incluso cuando se conocen en tiempo de compilación.
Martin Algesten
5

Porque no puedes Seriamente. Eso podría agregarse a la especificación del idioma. No ha sido Agregaría algo de complejidad. Ese beneficio para el costo significa que no es una alta prioridad.

Actualización: Actualmente se está agregando al lenguaje bajo JEP 301: Enums mejorados .

Tom Hawtin - tackline
fuente
¿Podría dar más detalles sobre las complicaciones?
Mr_and_Mrs_D
44
He estado pensando en esto durante un par de días y todavía no veo complicaciones.
Marko Topolnik el
4

Hay otros métodos en ENUM que no funcionarían. ¿Qué MyEnum.values()volvería?

¿Qué hay de MyEnum.valueOf(String name)?

Por el valor de si crees que el compilador podría hacer un método genérico como

public static MyEnum valueOf (nombre de cadena);

para llamarlo así MyEnum<String> myStringEnum = MyEnum.value("some string property"), tampoco funcionaría. Por ejemplo, ¿qué pasa si llamas MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")? No es posible implementar ese método para que funcione correctamente, por ejemplo, para lanzar una excepción o devolver un valor nulo cuando lo llama como MyEnum.<Int>value("some double property")debido a la eliminación de tipo.

usuario1944408
fuente
2
¿Por qué no iban a funcionar? Simplemente usarían comodines ...: MyEnum<?>[] values()yMyEnum<?> valueOf(...)
Lukas Eder
1
Pero entonces no podría hacer una tarea como esta MyEnum<Int> blabla = valueOf("some double property");porque los tipos no son compatibles. También, en ese caso, desea obtener un valor nulo porque desea devolver MyEnum <Int> que no existe para el nombre de propiedad doble y no puede hacer que ese método funcione correctamente debido a la eliminación.
user1944408
Además, si realiza un bucle a través de los valores (), necesitaría usar MyEnum <?>, Que generalmente no es lo que desea porque, por ejemplo, no puede recorrer solo sus propiedades Int. También necesitaría hacer un montón de casting que desea evitar, sugeriría crear diferentes enumeraciones para cada tipo o hacer su propia clase con instancias ...
user1944408
Bueno, supongo que no puedes tener ambas. Por lo general, recurro a mis propias implementaciones "enum". Es solo que Enumtiene muchas otras características útiles, y esta también sería opcional y muy útil ...
Lukas Eder
0

Francamente, esto parece más una solución en busca de un problema que otra cosa.

El propósito completo de la enumeración de Java es modelar una enumeración de instancias de tipo que comparten propiedades similares de una manera que proporciona consistencia y riqueza más allá de las representaciones de cadenas o enteros comparables.

Tome un ejemplo de una enumeración de libros de texto. Esto no es muy útil o consistente:

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

¿Por qué querría que mis diferentes planetas tengan diferentes conversiones de tipo genérico? ¿Qué problema soluciona? ¿Justifica complicar la semántica del lenguaje? Si necesito este comportamiento, ¿es una enumeración la mejor herramienta para lograrlo?

Además, ¿cómo gestionaría conversiones complejas?

por ejemplo

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

Es bastante fácil String Integerproporcionar el nombre u ordinal. Pero los genéricos le permitirían suministrar cualquier tipo. ¿Cómo gestionarías la conversión MyComplexClass? Ahora está compilando dos construcciones forzando al compilador a saber que hay un subconjunto limitado de tipos que se pueden suministrar a enumeraciones genéricas e introduciendo confusión adicional al concepto (genéricos) que ya parece eludir a muchos programadores.

nsfyn55
fuente
17
Pensar en un par de ejemplos en los que no sería útil es un argumento terrible de que nunca sería útil.
Elias Vasylenko
1
Los ejemplos apoyan el punto. Las instancias de enumeración son subclases del tipo (agradable y simple) y eso incluye genéricos es una lata de gusanos, términos de complejidad para un beneficio muy oscuro. Si vas a votarme a mí mismo, también deberías votar a Tom Hawtin a continuación, quien dijo lo mismo en pocas palabras
nsfyn55
1
@ nsfyn55 Las enumeraciones son una de las funciones de lenguaje más complejas y mágicas de Java. No hay una sola característica de otro idioma que genere automáticamente métodos estáticos, por ejemplo. Además, cada tipo de enumeración ya es una instancia de un tipo genérico.
Marko Topolnik el
1
@ nsfyn55 El objetivo del diseño era hacer que los miembros de enum fueran potentes y flexibles, por lo que admiten funciones tan avanzadas como métodos de instancia personalizados e incluso variables de instancia mutables. Están diseñados para tener un comportamiento especializado para cada miembro individualmente para que puedan participar como colaboradores activos en escenarios de uso complejos. Sobre todo, la enumeración de Java fue diseñada para hacer que el tipo de idioma de enumeración general sea seguro , pero la falta de parámetros de tipo desafortunadamente hace que no alcance ese objetivo tan preciado. Estoy bastante convencido de que había una razón muy específica para eso.
Marko Topolnik
1
No noté tu mención de contravarianza ... Java admite, solo es el sitio de uso, lo que lo hace un poco difícil de manejar. 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); }Tenemos que calcular manualmente cada vez qué tipo se consume y cuál se produce, y especificar los límites en consecuencia.
Marko Topolnik
-2

Porque "enum" es la abreviatura de enumeración. Es solo un conjunto de constantes con nombre que se colocan en lugar de números ordinales para hacer que el código sea mejor legible.

No veo cuál podría ser el significado previsto de una constante parametrizada por tipo.

Ingo
fuente
1
En java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. ¿Ahora ves? :-)
Lukas Eder
44
Dijiste que no ves cuál podría ser el significado de una constante parametrizada por tipo. Entonces le mostré una constante parametrizada por tipo (no un puntero de función).
Lukas Eder
Por supuesto que no, ya que el lenguaje java no conoce un puntero de función. Aún así, no la constante es tipo parametrizada, pero es tipo. Y el parámetro de tipo en sí es constante, no una variable de tipo.
Ingo
Pero la sintaxis enum es solo azúcar sintáctico. Debajo, son exactamente como CASE_INSENSITIVE_ORDER... Si Comparator<T>fuera una enumeración, ¿por qué no tener literales con ningún límite asociado <T>?
Lukas Eder
Si Comparator <T> fuera una enumeración, entonces podríamos tener todo tipo de cosas raras. Quizás algo como Integer <T>. Pero no lo es.
Ingo
-3

Creo que porque básicamente las enumeraciones no pueden ser instanciadas

¿Dónde establecerías la clase T si JVM te lo permitiera?

La enumeración es información que se supone que es siempre la misma, o al menos, que no cambiará dinámicamente.

nuevo MyEnum <> ()?

Aún así, el siguiente enfoque puede ser útil

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;
    }   
}
capsula
fuente
8
No estoy seguro de que me guste el setO()método en un enum. Considero constantes enumeraciones y para mí eso implica inmutable . Entonces, incluso si es posible, no lo haría.
Martin Algesten
2
Las enumeraciones se instancian en el código generado por el compilador. Cada literal se crea realmente llamando a los constructores privados. Por lo tanto, habría una posibilidad de pasar el tipo genérico al constructor en tiempo de compilación.
Lukas Eder
2
Los colocadores en enumeraciones son completamente normales. Especialmente si está usando la enumeración para crear un singleton (Elemento 3 Java efectivo por Joshua Bloch)
Preston