Por que os literais Java enum não deveriam ter parâmetros de tipo genérico?

148

Enums de Java são ótimas. O mesmo acontece com os genéricos. É claro que todos sabemos as limitações deste último por causa do apagamento de tipo. Mas há uma coisa que não entendo: por que não consigo criar uma enumeração como esta:

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

Esse parâmetro de tipo genérico, <T>por sua vez, pode ser útil em vários locais. Imagine um parâmetro de tipo genérico para um método:

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

Ou mesmo na própria classe enum:

public T convert(Object o);

Exemplo mais concreto nº 1

Como o exemplo acima pode parecer abstrato demais para alguns, aqui está um exemplo mais real do porquê eu quero fazer isso. Neste exemplo eu quero usar

  • Enums, porque então eu posso enumerar um conjunto finito de chaves de propriedade
  • Genéricos, porque então eu posso ter segurança de tipo no nível do método para armazenar propriedades
public interface MyProperties {
     public <T> void put(MyEnum<T> key, T value);
     public <T> T get(MyEnum<T> key);
}

Exemplo mais concreto # 2

Eu tenho uma enumeração de tipos de dados:

public interface DataType<T> {}

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

Cada literal de enumeração obviamente teria propriedades adicionais baseadas no tipo genérico <T>, enquanto ao mesmo tempo seria uma enumeração (imutável, singleton, enumerável, etc. etc.)

Questão:

Ninguém pensou nisso? Isso é uma limitação relacionada ao compilador? Considerando o fato de que a palavra-chave " enum " é implementada como açúcar sintático, representando o código gerado para a JVM, não entendo essa limitação.

Quem pode me explicar isso? Antes de responder, considere o seguinte:

  • Eu sei que tipos genéricos são apagados :-)
  • Eu sei que existem soluções alternativas usando objetos de classe. Eles são soluções alternativas.
  • Tipos genéricos resultam em transmissões de tipos gerados pelo compilador sempre que aplicável (por exemplo, ao chamar o método convert ()
  • O tipo genérico <T> estaria na enumeração. Portanto, ele é vinculado por cada um dos literais da enumeração. Portanto, o compilador saberia qual tipo aplicar ao escrever algo comoString string = LITERAL1.convert(myObject); Integer integer = LITERAL2.convert(myObject);
  • O mesmo se aplica ao parâmetro de tipo genérico no T getvalue()método O compilador pode aplicar a conversão de tipo ao chamarString string = someClass.getValue(LITERAL1)
Lukas Eder
fonte
3
Também não entendo essa limitação. Me deparei com isso recentemente, onde minha enum continha diferentes tipos "Comparáveis" e, com genéricos, apenas tipos comparáveis ​​dos mesmos tipos podem ser comparados sem avisos que precisem ser suprimidos (mesmo que em tempo de execução os comparáveis ​​sejam comparados). Eu poderia ter me livrado desses avisos usando um tipo vinculado na enum para especificar qual tipo comparável era suportado, mas, em vez disso, tive que adicionar a anotação SuppressWarnings - de maneira alguma! Desde compareTo faz lançar uma exceção de lançamento de classe de qualquer maneira, é ok eu acho, mas ainda assim ...
MetroidFan2002
5
(+1) Estou no meio da tentativa de fechar uma lacuna de segurança de tipo no meu projeto, interrompida por essa limitação arbitrária. Apenas considere o seguinte: transforme o enumidioma em "typesafe enum" que usamos antes do Java 1.5. De repente, você pode ter seus membros da enum parametrizados. Provavelmente é o que vou fazer agora.
Marko Topolnik
1
@ EdwinDalorzo: Atualizamos a pergunta com um exemplo concreto do jOOQ , onde isso teria sido imensamente útil no passado.
Lukas Eder #
2
@LukasEder Eu vejo o seu ponto agora. Parece um novo recurso interessante. Talvez você deva sugerir isso na lista de discussão sobre moedas do projeto . Vejo outras propostas interessantes nas enumerações, mas nenhuma como a sua.
Edwin Dalorzo 4/03/15
1
Concordo plenamente. Enum sem genéricos são aleijados. O seu caso nº 1 também é meu. Se eu precisar de uma enumeração genérica, desisto do JDK5 e o implemento no estilo antigo do Java 1.4. Essa abordagem também traz benefícios adicionais : não sou obrigado a ter todas as constantes em uma classe ou mesmo pacote. Assim, o estilo de pacote por recurso é realizado muito melhor. Isso é perfeito apenas para "enumerações" de configuração - constantes são distribuídas em pacotes de acordo com seu significado lógico (e se eu quiser ver todas elas, tenho o tipo hieararchy mostrado).
Tomáš Záluský 4/03/2015

Respostas:

50

Agora isso está sendo discutido no JEP-301 Enhanced Enums . O exemplo dado no JEP é exatamente o que eu estava procurando:

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

Infelizmente, o JEP ainda está enfrentando problemas significativos: http://mail.openjdk.java.net/pipermail/amber-spec-experts/2017-May/000041.html

Lukas Eder
fonte
2
Em dezembro de 2018, havia alguns sinais de vida em torno do JEP 301 novamente , mas ao examinar essa discussão, fica claro que os problemas ainda estão longe de serem resolvidos.
Pont
11

A resposta está na pergunta:

por causa do tipo de apagamento

Nenhum desses dois métodos é possível, pois o tipo de argumento é apagado.

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

Para realizar esses métodos, você pode construir sua enumeração 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
fonte
2
Bem, o apagamento ocorre em tempo de compilação. Mas o compilador pode usar informações genéricas de tipo para verificações de tipo. E então "converte" o tipo genérico em tipos de conversão. Vou reformular a pergunta
Lukas Eder
2
Não tenho certeza se eu sigo inteiramente. Tome public T convert(Object);. Eu estou supondo que este método poderia, por exemplo, restringir um monte de tipos diferentes para <T>, que por exemplo é uma String. A construção do objeto String é em tempo de execução - nada que o compilador faça. Portanto, você precisa conhecer o tipo de tempo de execução, como String.class. Ou eu estou esquecendo de alguma coisa?
Martin Algesten
6
Acho que você está perdendo o fato de que o tipo genérico <T> está na enumeração (ou em sua classe gerada). As únicas instâncias da enumeração são seus literais, que fornecem uma ligação de tipo genérico constante. Portanto, não há ambiguidade sobre T no público T convert (Object);
Lukas Eder #
2
Aha! Entendi. Você está mais inteligente do que me :)
Martin Algesten
1
Eu acho que tudo se resume à classe de uma enumeração sendo avaliada de maneira semelhante a todo o resto. Em java, embora as coisas pareçam constantes, elas não são. Considere public final static String FOO;com um bloco estático static { FOO = "bar"; }- também as constantes são avaliadas mesmo quando conhecidas em tempo de compilação.
Martin Algesten 27/11
5

Porque você não pode. Seriamente. Isso pode ser adicionado à especificação do idioma. Não foi. Isso acrescentaria alguma complexidade. Esse benefício de custo significa que não é uma alta prioridade.

Atualização: atualmente sendo adicionada ao idioma no JEP 301: Enums aprimoradas .

Tom Hawtin - linha de orientação
fonte
Você poderia elaborar as complicações?
Mr_and_Mrs_D
4
Estou pensando nisso há alguns dias e ainda não vejo complicações.
Marko Topolnik
4

Existem outros métodos no ENUM que não funcionariam. O que seriaMyEnum.values() retornaria?

A respeito MyEnum.valueOf(String name) ?

Para o valueOf, se você acha que o compilador pode criar um método genérico como

public estático MyEnum valueOf (String name);

para chamar assim MyEnum<String> myStringEnum = MyEnum.value("some string property"), isso também não funcionaria. Por exemplo, e se você ligar MyEnum<Int> myIntEnum = MyEnum.<Int>value("some string property")? Não é possível implementar esse método para funcionar corretamente, por exemplo, para lançar uma exceção ou retornar nulo quando você o chama como MyEnum.<Int>value("some double property")por causa do tipo apagamento.

user1944408
fonte
2
Por que eles não funcionariam? Eles simplesmente usavam curingas ...: MyEnum<?>[] values()eMyEnum<?> valueOf(...)
Lukas Eder
1
Mas você não poderia fazer uma tarefa como essa MyEnum<Int> blabla = valueOf("some double property");porque os tipos não são compatíveis. Além disso, nesse caso, você deseja obter nulo porque deseja retornar MyEnum <Int> que não existe para o nome da propriedade dupla e não é possível fazer com que esse método funcione corretamente devido ao apagamento.
user1944408
Além disso, se você percorrer os valores (), precisará usar MyEnum <?>, Que não é o que você deseja normalmente, porque, por exemplo, você não pode percorrer apenas as propriedades Int. Além disso, você precisa fazer um monte de fundição que você quer evitar, gostaria de sugerir para criar diferentes enums para cada tipo ou fazer a sua própria classe com casos ...
user1944408
Bem, acho que você não pode ter os dois. Normalmente, eu recorro às minhas próprias implementações "enum". É só que Enumtem um monte de outras funcionalidades úteis, e este seria opcional e muito útil, bem ...
Lukas Eder
0

Francamente, isso parece mais uma solução em busca de um problema do que qualquer coisa.

O objetivo inteiro do java enum é modelar uma enumeração de instâncias de tipo que compartilhem propriedades semelhantes de uma maneira que forneça consistência e riqueza além das representações String ou Inteiro comparáveis.

Veja um exemplo de enum de livro de texto. Isso não é muito útil ou consistente:

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

Por que eu gostaria que meus planetas diferentes tivessem conversões de tipos genéricos diferentes? Qual problema isso resolve? Justifica complicar a semântica da linguagem? Se eu precisar desse comportamento, um enum é a melhor ferramenta para alcançá-lo?

Além disso, como você gerenciaria conversões complexas?

por exemplo

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

É fácil o suficiente String Integerpara fornecer o nome ou ordinal. Mas os genéricos permitiriam fornecer qualquer tipo. Como você gerenciaria a conversão MyComplexClass? Agora, você está criando duas construções forçando o compilador a saber que há um subconjunto limitado de tipos que podem ser fornecidos a enumerações genéricas e introduzindo uma confusão adicional ao conceito (Genéricos) que já parece iludir muitos programadores.

nsfyn55
fonte
17
Pensar em alguns exemplos em que isso não seria útil é um argumento terrível de que nunca seria útil.
Elias Vasylenko
1
Os exemplos sustentam o argumento. As instâncias enum são subclasses do tipo (agradável e simples) e que incluir genéricos é uma lata de worms e termos de complexidade para um benefício muito obscuro. Se você estiver indo para me downvote então você também deve downvote Tom Hawtin abaixo que disse a mesma coisa em não tantas palavras
nsfyn55
1
@ nsfyn55 As enums são um dos recursos de linguagem mais complexos e mágicos do Java. Não há um único recurso de outro idioma que gere automaticamente métodos estáticos, por exemplo. Além disso, cada tipo de enum já é uma instância de um tipo genérico.
Marko Topolnik
1
@ nsfyn55 O objetivo do projeto era tornar os membros da enum poderosos e flexíveis, e é por isso que eles oferecem suporte a recursos avançados, como métodos de instância personalizados e até variáveis ​​de instância mutáveis. Eles são projetados para ter um comportamento especializado para cada membro individualmente, para que possam participar como colaboradores ativos em cenários de uso complexos. Acima de tudo, o enum Java foi projetado para tornar o tipo de idioma de enumeração geral seguro , mas a falta de parâmetros de tipo infelizmente o deixa aquém do objetivo mais estimado. Estou bastante convencido de que havia uma razão muito específica para isso.
Marko Topolnik
1
Eu não notei sua menção de contravariância ... Java é compatível, apenas o site de uso, o que o torna um pouco pesado. 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); }Temos que trabalhar manualmente cada vez que tipo é consumido e qual produzido e especificar os limites de acordo.
Marko Topolnik
-2

Porque "enum" é a abreviação de enumeração. É apenas um conjunto de constantes nomeadas que substituem os números ordinais para tornar o código melhor legível.

Não vejo qual poderia ser o significado pretendido de uma constante parametrizada por tipo.

Ingo
fonte
1
Em java.lang.String: public static final Comparator<String> CASE_INSENSITIVE_ORDER. Você vê agora? :-)
Lukas Eder
4
Você disse que não vê qual poderia ser o significado de uma constante parametrizada por tipo. Então eu mostrei uma constante parametrizada por tipo (não um ponteiro de função).
Lukas Eder
Claro que não, pois a linguagem java não sabe o que é um ponteiro de função. Ainda assim, a constante não é do tipo parametrizada, mas é do tipo. E o próprio parâmetro de tipo é constante, não uma variável de tipo.
Ingo
Mas a sintaxe do enum é apenas açúcar sintático. Por baixo, eles são exatamente como CASE_INSENSITIVE_ORDER... Se Comparator<T>havia um enum, por que não ter literais com qualquer associado associado <T>?
Lukas Eder
Se o Comparator <T> fosse um enum, então poderíamos ter todo tipo de coisas estranhas. Talvez algo como Inteiro <T>. Mas não é.
Ingo
-3

Eu acho que porque basicamente Enums não podem ser instanciados

Onde você definiria a classe T, se a JVM permitisse?

Enumeração são dados que devem ser sempre os mesmos, ou pelo menos, que não serão alterados dinamicamente.

novo MyEnum <> ()?

Ainda a seguinte abordagem pode 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;
    }   
}
cápsula
fonte
8
Não tenho certeza se eu gosto do setO()método em um enum. Considero constantes enum e para mim isso implica imutável . Então, mesmo que seja possível, eu não faria isso.
Martin Algesten
2
As enumerações são instanciadas no código gerado pelo compilador. Cada literal é realmente criado chamando os construtores privados. Portanto, haveria uma chance de passar o tipo genérico para o construtor em tempo de compilação.
Lukas Eder
2
Os setters em enums são completamente normais. Especialmente se você estiver usando o enum para criar um Singleton (Item 3 Effective Java por Joshua Bloch)
Preston