Por que devo me preocupar se Java não tem genéricos reificados?

102

Isso surgiu como uma pergunta que fiz em uma entrevista recentemente, como algo que o candidato gostaria de ver adicionado à linguagem Java. É comumente identificado como uma dor que o Java não reificou os genéricos , mas, quando pressionado, o candidato não poderia realmente me dizer o tipo de coisas que ele poderia ter alcançado se eles estivessem lá.

Obviamente, como os tipos brutos são permitidos em Java (e verificações inseguras), é possível subverter os genéricos e acabar com um List<Integer>que (por exemplo) realmente contém Strings. Isso claramente poderia se tornar impossível se as informações de tipo fossem reificadas; mas deve haver mais do que isso !

As pessoas poderiam postar exemplos de coisas que realmente gostariam de fazer , se houvesse genéricos reificados disponíveis? Quero dizer, obviamente você poderia obter o tipo de a Listem tempo de execução - mas o que você faria com ele?

public <T> void foo(List<T> l) {
   if (l.getGenericType() == Integer.class) {
       //yeah baby! err, what now?

EDITAR : Uma atualização rápida para isso, pois as respostas parecem estar principalmente preocupadas com a necessidade de passar um Classcomo um parâmetro (por exemplo EnumSet.noneOf(TimeUnit.class)). Eu estava procurando mais por algo onde isso simplesmente não fosse possível . Por exemplo:

List<?> l1 = api.gimmeAList();
List<?> l2 = api.gimmeAnotherList();

if (l1.getGenericType().isAssignableFrom(l2.getGenericType())) {
    l1.addAll(l2); //why on earth would I be doing this anyway?
oxbow_lakes
fonte
Isso significa que você poderia obter a classe de um tipo genérico em tempo de execução? (em caso afirmativo, tenho um exemplo!)
James B
Acho que a maior parte do desejo com genéricos reificáveis ​​vem de pessoas que usam genéricos principalmente com coleções e desejam que essas coleções se comportem mais como matrizes.
kdgregory
2
A pergunta mais interessante (para mim): o que seria necessário para implementar genéricos de estilo C ++ em Java? Certamente parece possível em tempo de execução, mas quebraria todos os carregadores de classe existentes (porque findClass()teria que ignorar a parametrização, mas defineClass()não poderia). E como sabemos, The Powers That Be considera a compatibilidade com versões anteriores primordial.
kdgregory
1
Na verdade, o Java fornece genéricos reificados de uma forma muito restrita . Forneço mais detalhes neste tópico do SO: stackoverflow.com/questions/879855/…
Richard Gomes
O JavaOne Keynote indica que o Java 9 oferecerá suporte à reificação.
Rangi Keen

Respostas:

81

Nas poucas vezes em que me deparei com essa "necessidade", no final das contas tudo se resume a esta construção:

public class Foo<T> {

    private T t;

    public Foo() {
        this.t = new T(); // Help?
    }

}

Isso funciona em C #, supondo que Ttenha um construtor padrão . Você pode até obter o tipo de tempo de execução typeof(T)e obter os construtores Type.GetConstructor().

A solução Java comum seria passar o Class<T>argumento as.

public class Foo<T> {

    private T t;

    public Foo(Class<T> cls) throws Exception {
        this.t = cls.newInstance();
    }

}

(não precisa necessariamente ser passado como argumento do construtor, já que um argumento de método também é bom, o acima é apenas um exemplo, também try-catché omitido por questões de brevidade)

Para todas as outras construções de tipo genérico, o tipo real pode ser facilmente resolvido com um pouco de ajuda de reflexão. As perguntas e respostas abaixo ilustram os casos de uso e possibilidades:

BalusC
fonte
5
Isso funciona (por exemplo) em C #? Como você sabe que T tem um construtor padrão?
Thomas Jung
4
Sim faz. E este é apenas um exemplo básico (vamos supor que trabalhemos com JavaBeans). O ponto principal é que com Java você não pode obter a classe durante o tempo de execução por T.classou T.getClass(), de forma que você possa acessar todos os seus campos, construtores e métodos. Também torna a construção impossível.
BalusC
3
Isso parece muito fraco para mim como o "grande problema", particularmente porque só é provável que seja útil em conjunto com alguma reflexão muito frágil em torno de construtores / parâmetros etc.
oxbow_lakes
33
Ele é compilado em C #, desde que você declare o tipo como: public class Foo <T> onde T: new (). O que limitará os tipos válidos de T àqueles que contêm um construtor sem parâmetros.
Martin Harris
3
@Colin Você realmente pode dizer ao java que um determinado tipo genérico é necessário para estender mais de uma classe ou interface. Consulte a seção "Multiple Bounds" de docs.oracle.com/javase/tutorial/java/generics/bounded.html . (Claro, você não pode dizer que o objeto será um de um conjunto específico que não compartilha métodos que poderiam ser abstraídos em uma interface, que poderia ser implementada em C ++ usando a especialização de modelo.)
JAB
99

O que mais comumente me incomoda é a incapacidade de tirar proveito do envio múltiplo em vários tipos genéricos. O seguinte não é possível e há muitos casos em que seria a melhor solução:

public void my_method(List<String> input) { ... }
public void my_method(List<Integer> input) { ... }
RHSeeger
fonte
5
Não há absolutamente nenhuma necessidade de reificação para ser capaz de fazer isso. A seleção do método é feita em tempo de compilação quando as informações de tipo em tempo de compilação estão disponíveis.
Tom Hawtin - tackline
26
@Tom: isto nem mesmo compila por causa do apagamento de tipo. Ambos são compilados como public void my_method(List input) {}. No entanto, nunca me deparei com essa necessidade, simplesmente porque não teriam o mesmo nome. Se eles tiverem o mesmo nome, eu questiono se public <T extends Object> void my_method(List<T> input) {}não é uma ideia melhor.
BalusC
1
Hm, eu tenderia a evitar a sobrecarga com um número idêntico de parâmetros completamente e preferiria algo como myStringsMethod(List<String> input)e myIntegersMethod(List<Integer> input)até mesmo se a sobrecarga para tal caso fosse possível em Java.
Fabian Steeg
4
@Fabian: O que significa que você precisa ter um código separado e evitar o tipo de vantagens que obtém <algorithm>em C ++.
David Thornley
Estou confuso, este é essencialmente o mesmo que meu segundo ponto, mas consegui 2 votos negativos sobre ele? Alguém se importa em me esclarecer?
rsp
34

A segurança do tipo vem à mente. O downcasting para um tipo parametrizado sempre será inseguro sem genéricos reificados:

List<String> myFriends = new ArrayList();
myFriends.add("Alice");
getSession().put("friends", myFriends);
// later, elsewhere
List<Friend> myFriends = (List<Friend>) getSession().get("friends");
myFriends.add(new Friend("Bob")); // works like a charm!
// and so...
List<String> myFriends = (List<String>) getSession().get("friends");
for (String friend : myFriends) print(friend); // ClassCastException, wtf!? 

Além disso, as abstrações vazariam menos - pelo menos aquelas que podem estar interessadas em informações de tempo de execução sobre seus parâmetros de tipo. Hoje, se você precisa de qualquer tipo de informação de tempo de execução sobre o tipo de um dos parâmetros genéricos, você também deve transmiti-la Class. Dessa forma, sua interface externa depende de sua implementação (se você usa RTTI sobre seus parâmetros ou não).

gustafc
fonte
1
Sim - eu tenho uma maneira de contornar isso: crio um ParametrizedListque copia os dados nos tipos de verificação da coleção de origem. É um pouco parecido, Collections.checkedListmas pode ser semeado com uma coleção para começar.
oxbow_lakes
@tackline - bem, algumas abstrações vazariam menos. Se você precisa de acesso para digitar metadados em sua implementação, a interface externa o denuncia porque os clientes precisam enviar um objeto de classe.
gustafc
... significando que com genéricos reificados, você pode adicionar coisas como T.class.getAnnotation(MyAnnotation.class)(onde Té um tipo genérico) sem alterar a interface externa.
gustafc
@gustafc: se você acha que os modelos C ++ oferecem segurança de tipo completa, leia isto: kdgregory.com/index.php?page=java.generics.cpp
kdgregory
@kdgregory: Eu nunca disse que C ++ era 100% seguro para tipos - apenas que o apagamento danifica a segurança de tipos. Como você mesmo diz, "C ++, ao que parece, tem sua própria forma de eliminação de tipo, conhecida como conversão de ponteiro no estilo C." Mas Java só faz conversões dinâmicas (não reinterpretando), então a reificação conectaria tudo isso ao sistema de tipos.
gustafc
26

Você seria capaz de criar matrizes genéricas em seu código.

public <T> static void DoStuff() {
    T[] myArray = new T[42]; // No can do
}
Turnor
fonte
o que há de errado com o objeto? De qualquer forma, uma matriz de objetos é uma matriz de referências. Não é como se os dados do objeto estivessem na pilha - estão todos na pilha.
Ran Biron
19
Segurança de tipo. Posso colocar o que quiser em Object [], mas apenas Strings em String [].
Turnor
3
Ran: Sem ser sarcástico: você pode gostar de usar uma linguagem de script em vez de Java, então você tem a flexibilidade de variáveis ​​sem tipo em qualquer lugar!
ovelhas voadoras
2
As matrizes são covariantes (e, portanto, não seguras para tipos) em ambas as linguagens. String[] strings = new String[1]; Object[] objects = strings; objects[0] = new Object();Compila bem em ambos os idiomas. Executa não muito bem.
Martijn
16

Esta é uma pergunta antiga, há uma tonelada de respostas, mas acho que as respostas existentes estão erradas.

"reificado" significa apenas real e geralmente significa apenas o oposto de apagamento de tipo.

O grande problema relacionado ao Java Generics:

  • Esta horrível exigência de boxe e desconexão entre primitivos e tipos de referência. Isso não está diretamente relacionado à reificação ou eliminação de tipo. C # / Scala corrige isso.
  • Sem tipos próprios. O JavaFX 8 teve que remover "construtores" por esse motivo. Absolutamente nada a ver com eliminação de tipo. Scala corrige isso, não tenho certeza sobre C #.
  • Sem variação de tipo do lado da declaração. C # 4.0 / Scala tem isso. Absolutamente nada a ver com eliminação de tipo.
  • Não pode sobrecarregar void method(List<A> l)e method(List<B> l). Isso ocorre devido ao apagamento do tipo, mas é extremamente mesquinho.
  • Sem suporte para reflexão de tipo de tempo de execução. Este é o cerne da eliminação de tipo. Se você gosta de compiladores superavançados que verificam e provam o máximo da lógica do seu programa em tempo de compilação, você deve usar o mínimo de reflexão possível e este tipo de apagamento não deve incomodá-lo. Se você gosta de programação de tipo dinâmico, com scripts e mais remendado e não se importa muito com um compilador provando o máximo possível de sua lógica correta, então você deseja uma melhor reflexão e corrigir o apagamento de tipo é importante.
user2684301
fonte
1
Geralmente acho difícil com casos de serialização. Freqüentemente, você gostaria de ser capaz de farejar os tipos de classe de coisas genéricas sendo serializadas, mas você é interrompido por causa do apagamento de tipo. Isso torna difícil fazer algo assimdeserialize(thingy, List<Integer>.class)
Cogman
3
Acho que essa é a melhor resposta. Especialmente as partes que descrevem quais problemas são realmente fundamentais devido ao apagamento de tipo e quais são apenas problemas de design da linguagem Java. A primeira coisa que digo às pessoas que começam a lamentar-se é que Scala e Haskell também estão trabalhando com eliminação de tipos.
aemxdp
13

A serialização seria mais direta com a reificação. O que nós queremos é

deserialize(thingy, List<Integer>.class);

O que temos que fazer é

deserialize(thing, new TypeReference<List<Integer>>(){});

parece feio e funciona mal.

Também há casos em que seria muito útil dizer algo como

public <T> void doThings(List<T> thingy) {
    if (T instanceof Q)
      doCrazyness();
  }

Essas coisas não picam com frequência, mas picam quando acontecem.

Cogman
fonte
Este. Mil vezes isso. Toda vez que tento escrever código de desserialização em Java, passo o dia lamentando que não estou trabalhando em outra coisa
Básico
11

Minha exposição ao Java Geneircs é bastante limitada e, além dos pontos que outras respostas já mencionaram, há um cenário explicado no livro Java Generics and Collections , de Maurice Naftalin e Philip Walder, onde os genéricos reificados são úteis.

Visto que os tipos não são reificáveis, não é possível ter exceções parametrizadas.

Por exemplo, a declaração do formulário abaixo não é válida.

class ParametericException<T> extends Exception // compile error

Isto é porque a captura cláusula verifica se a exceção lançada corresponde a um determinado tipo. Esta verificação é igual à verificação realizada pelo teste de instância e, como o tipo não é reificável, a forma de declaração acima é inválida.

Se o código acima fosse válido, o tratamento de exceções da maneira abaixo teria sido possível:

try {
     throw new ParametericException<Integer>(42);
} catch (ParametericException<Integer> e) { // compile error
  ...
}

O livro também menciona que, se os genéricos Java forem definidos de maneira semelhante à forma como os modelos C ++ são definidos (expansão), isso pode levar a uma implementação mais eficiente, pois oferece mais oportunidades de otimização. Mas não oferece nenhuma explicação mais do que isso, então qualquer explicação (dicas) de pessoas experientes seria útil.

sateesh
fonte
1
É um ponto válido, mas não tenho certeza de por que uma classe de exceção tão parametrizada seria útil. Você poderia modificar sua resposta para conter um breve exemplo de quando isso pode ser útil?
oxbow_lakes
@oxbow_lakes: Desculpe, meu conhecimento do Java Generics é bastante limitado e estou tentando melhorá-lo. Portanto, agora não consigo pensar em nenhum exemplo em que a exceção parametrizada possa ser útil. Vou tentar pensar sobre isso. THX.
sábado
Ele pode atuar como um substituto para a herança múltipla de tipos de exceção.
Meriton
A melhoria de desempenho é que, atualmente, um parâmetro de tipo deve ser herdado de Object, exigindo boxing de tipos primitivos, o que impõe uma sobrecarga de execução e memória.
Meriton
8

Os arrays provavelmente funcionariam muito melhor com os genéricos se fossem reificados.

Hank Gay
fonte
2
Claro, mas eles ainda teriam problemas. List<String>não é um List<Object>.
Tom Hawtin - tackline
Concordo - mas apenas para primitivos (Inteiro, Longo, etc). Para Objetos "regulares", é o mesmo. Como os primitivos não podem ser um tipo parametrizado (um problema muito mais sério, pelo menos IMHO), não vejo isso como uma verdadeira dor.
Ran Biron
3
O problema com matrizes é sua covariância, nada a ver com reificação.
Recurse em
5

Eu tenho um wrapper que apresenta um conjunto de resultados jdbc como um iterador (isso significa que posso testar a unidade de operações originadas do banco de dados muito mais facilmente por meio da injeção de dependência).

A API parece Iterator<T> onde T é algum tipo que pode ser construído usando apenas strings no construtor. O Repetidor, então, olha para as strings que estão sendo retornadas da consulta sql e tenta combiná-las com um construtor do tipo T.

Na forma atual como os genéricos são implementados, também tenho que passar a classe dos objetos que estarei criando a partir do meu conjunto de resultados. Se bem entendi, se os genéricos fossem reificados, eu poderia simplesmente chamar T.getClass () obter seus construtores e, em seguida, não ter que lançar o resultado de Class.newInstance (), o que seria muito mais puro.

Basicamente, acho que torna a escrita de APIs (em oposição a apenas escrever um aplicativo) mais fácil, porque você pode inferir muito mais de objetos e, portanto, menos configuração será necessária ... Eu não apreciei as implicações das anotações até que os vi sendo usados ​​em coisas como spring ou xstream em vez de resmas de configuração.

James B
fonte
Mas passar na aula parece mais seguro para mim. Em qualquer caso, a criação reflexiva de instâncias a partir de consultas de banco de dados é extremamente frágil para mudanças como a refatoração de qualquer maneira (no código e no banco de dados). Acho que estava procurando coisas em que simplesmente não é possível fornecer a classe
oxbow_lakes
5

Uma coisa boa seria evitar boxing para tipos primitivos (valor). Isso está um tanto relacionado à reclamação de array que outras pessoas levantaram e, nos casos em que o uso de memória é restrito, pode realmente fazer uma diferença significativa.

Existem também vários tipos de problemas ao escrever uma estrutura onde é importante ser capaz de refletir sobre o tipo parametrizado. É claro que isso pode ser contornado passando um objeto de classe em tempo de execução, mas isso obscurece a API e coloca uma carga adicional no usuário do framework.

kvb
fonte
2
Isso basicamente se resume a ser capaz de fazer new T[]onde T é do tipo primitivo!
oxbow_lakes
2

Não é que você vai conseguir algo extraordinário. Será apenas mais simples de entender. O apagamento de tipo parece ser um momento difícil para iniciantes e, em última análise, requer a compreensão da maneira como o compilador funciona.

Minha opinião é que os genéricos são simplesmente um extra que economiza muito elenco redundante.

Bozho
fonte
Isso é certamente o que os genéricos são em Java . Em C # e outras linguagens, eles são uma ferramenta poderosa
Básico
1

Algo que todas as respostas aqui perderam e que é constantemente uma dor de cabeça para mim é que, uma vez que os tipos são apagados, você não pode herdar uma interface genérica duas vezes. Isso pode ser um problema quando você deseja fazer interfaces de granulação fina.

    public interface Service<KEY,VALUE> {
           VALUE get(KEY key);
    }

    public class PersonService implements Service<Long, Person>,
        Service<String, Person> //Can not do!!
ExCodeCowboy
fonte
0

Aqui está um que me pegou hoje: sem reificação, se você escrever um método que aceita uma lista varargs de itens genéricos ... os chamadores podem PENSAR que são à prova de texto, mas acidentalmente passam qualquer coisa velha e explodem seu método.

Parece improvável que isso aconteça? ... Claro, até ... você usar Class como seu tipo de dados. Nesse ponto, seu chamador ficará feliz em enviar muitos objetos Class, mas um simples erro de digitação enviará objetos Class que não aderem a T e ocorrerá um desastre.

(NB: posso ter cometido um erro aqui, mas pesquisando "varargs genéricos" acima parece ser exatamente o que você esperaria. O que torna este um problema prático é o uso de Class, eu acho - chamadores parecem para ser menos cuidadoso :()


Por exemplo, estou usando um paradigma que usa objetos Class como uma chave em mapas (é mais complexo do que um mapa simples - mas conceitualmente é isso que está acontecendo).

por exemplo, isso funciona muito bem em Genéricos Java (exemplo trivial):

public <T extends Component> Set<UUID> getEntitiesPossessingComponent( Class<T> componentType)
    {
        // find the entities that are mapped (somehow) from that class. Very type-safe
    }

por exemplo, sem reificação no Java Generics, este aceita QUALQUER objeto "Class". E é apenas uma pequena extensão do código anterior:

public <T extends Component> Set<UUID> getEntitiesPossessingComponents( Class<T>... componentType )
    {
        // find the entities that are mapped (somehow) to ALL of those classes
    }

Os métodos acima devem ser escritos milhares de vezes em um projeto individual - portanto, a possibilidade de erro humano aumenta. Depurar erros está provando que "não é divertido". No momento, estou tentando encontrar uma alternativa, mas não tenho muita esperança.

Adão
fonte