Por que alguns afirmam que a implementação de genéricos do Java é ruim?


115

Ocasionalmente, ouvi dizer que, com os genéricos, Java não acertou. (referência mais próxima, aqui )

Perdoe minha inexperiência, mas o que os teria melhorado?


23
Não vejo razão para fechar isso; com toda a porcaria que circula sobre os genéricos, ter algum esclarecimento entre as diferenças entre Java e C # pode ajudar as pessoas que estão tentando se esquivar da confusão mal informada.
Rob

Respostas:


146

Ruim:

  • As informações de tipo são perdidas em tempo de compilação, então em tempo de execução você não pode dizer que tipo é "destinado" a ser
  • Não pode ser usado para tipos de valor (isso é importante - no .NET um List<byte>realmente é apoiado por um, byte[]por exemplo, e não é necessário encaixar)
  • Sintaxe para chamar métodos genéricos é uma merda (IMO)
  • A sintaxe para restrições pode ser confusa
  • Caracteres curinga geralmente são confusos
  • Várias restrições devido ao acima - elenco etc.

Boa:

  • O curinga permite que a covariância / contravariância seja especificada no lado da chamada, o que é muito bom em muitas situações
  • É melhor que nada!

51
@Paul: Acho que teria preferido uma falha temporária, mas depois uma API decente. Ou siga o caminho do .NET e introduza novos tipos de coleção. (Os genéricos .NET no 2.0 não danificaram os apps 1.1.)
Jon Skeet

6
Com uma grande base de código existente, gosto do fato de poder começar a alterar a assinatura de um método para usar genéricos sem alterar todos que o chamam.
Paul Tomblin

38
Mas as desvantagens disso são enormes e permanentes. Há uma grande vitória de curto prazo e uma perda de longo prazo IMO.
Jon Skeet

11
Jon - "É melhor do que nada" é subjetivo :)
MetroidFan2002

6
@Rogerio: Você alegou que meu primeiro item "Ruim" não está correto. Meu primeiro item "Ruim" diz que as informações são perdidas no momento da compilação. Para que isso esteja incorreto, você deve estar dizendo que a informação não é perdida em tempo de compilação ... Quanto a confundir outros desenvolvedores, você parece ser o único confuso com o que estou dizendo.
Jon Skeet

26

O maior problema é que os genéricos Java são uma coisa apenas em tempo de compilação e você pode subvertê-los em tempo de execução. C # é elogiado porque faz mais verificações de tempo de execução. Há uma discussão muito boa neste post , e ela leva a outras discussões.


Isso não é realmente um problema, nunca foi feito para ser executado. É como se você dissesse que os barcos são um problema porque não conseguem escalar montanhas. Como linguagem, Java faz o que muitas pessoas precisam: expressar tipos de maneira significativa. Para informações do tipo runtime, as pessoas ainda podem passar Classobjetos.
Vincent Cantin

1
Ele tem razão. sua anologia está errada porque as pessoas que projetaram o barco DEVERIAM tê-lo projetado para escalar montanhas. Seus objetivos de design não foram muito bem pensados ​​para o que era necessário. Agora estamos todos presos a apenas um barco e não podemos escalar montanhas.
WiegleyJ

1
@VincentCantin - é definitivamente um problema. Portanto, todos nós estamos reclamando disso. Os genéricos Java estão incompletos.
Josh M.

17

O principal problema é que o Java não possui genéricos em tempo de execução. É um recurso de tempo de compilação.

Quando você cria uma classe genérica em Java, eles usam um método chamado "Type Erasure" para realmente remover todos os tipos genéricos da classe e essencialmente substituí-los por Object. A versão milionária dos genéricos é que o compilador simplesmente insere conversões no tipo genérico especificado sempre que ele aparece no corpo do método.

Isso tem muitas desvantagens. Um dos maiores, IMHO, é que você não pode usar reflexão para inspecionar um tipo genérico. Os tipos não são realmente genéricos no código de bytes e, portanto, não podem ser inspecionados como genéricos.

Excelente visão geral das diferenças aqui: http://www.jprl.com/Blog/archive/development/2007/Aug-31.html


2
Não sei por que você foi rejeitado. Pelo que entendi você está certo, e esse link é muito informativo. Votado.
Jason Jackson

@Jason, obrigado. Houve um erro de digitação na minha versão original, acho que fui reprovado por isso.
JaredPar

3
Err, porque você pode usar reflexão para examinar um tipo genérico, uma assinatura de método genérica. Você simplesmente não pode usar reflexão para inspecionar as informações genéricas associadas a uma instância parametrizada. Por exemplo, se eu tiver uma classe com um método foo (List <String>), posso encontrar reflexivamente "String"
oxbow_lakes

Existem muitos artigos que dizem que o apagamento de tipo torna impossível encontrar os parâmetros de tipo reais. Vamos dizer: Objeto x = novo X [Y] (); você pode mostrar como, dado x, encontrar o tipo Y?
user48956

@oxbow_lakes - ter que usar reflexão para encontrar um tipo genérico é quase tão ruim quanto não ser capaz de fazer isso. Por exemplo, é uma solução ruim.
Josh M.

14
  1. Implementação em tempo de execução (ou seja, sem eliminação de tipo);
  2. A capacidade de usar tipos primitivos (isso está relacionado a (1));
  3. Embora o curinga seja útil, a sintaxe e saber quando usá-lo é algo que confunde muitas pessoas. e
  4. Sem melhoria de desempenho (por causa de (1); genéricos Java são açúcar sintático para lançar objetos).

(1) leva a um comportamento muito estranho. O melhor exemplo que posso pensar é. Presumir:

public class MyClass<T> {
  T getStuff() { ... }
  List<String> getOtherStuff() { ... }
}

em seguida, declare duas variáveis:

MyClass<T> m1 = ...
MyClass m2 = ...

Agora ligue getOtherStuff():

List<String> list1 = m1.getOtherStuff(); 
List<String> list2 = m2.getOtherStuff(); 

O segundo tem seu argumento de tipo genérico removido pelo compilador porque é um tipo bruto (o que significa que o tipo parametrizado não é fornecido), embora não tenha nada a ver com o tipo parametrizado.

Também mencionarei minha declaração favorita do JDK:

public class Enum<T extends Enum<T>>

Além do caractere curinga (que é uma mistura), só acho que os genéricos .Net são melhores.


Mas o compilador dirá a você, especialmente com a opção rawtypes lint.
Tom Hawtin - tackline

Desculpe, por que a última linha é um erro de compilação? Estou mexendo com isso no Eclipse e não consigo fazer com que falhe lá - se eu adicionar material suficiente para o resto compilar.
oconnor0

public class Redundancy<R extends Redundancy<R>>;)
java.is.for.desktop

@ oconnor0 Não é uma falha de compilação, mas um aviso do compilador, sem motivo aparente:The expression of type List needs unchecked conversion to conform to List<String>
artbristol

Enum<T extends Enum<T>>pode parecer estranho / redundante no início, mas é realmente muito interessante, pelo menos dentro das restrições de Java / seus genéricos. Enums têm um values()método estático que fornece uma matriz de seus elementos digitados como enum, não Enum, e esse tipo é determinado pelo parâmetro genérico, o que significa que você deseja Enum<T>. Obviamente, essa digitação só faz sentido no contexto de um tipo enumerado, e todos os enums são subclasses de Enum, assim você deseja Enum<T extends Enum>. No entanto, Java não gosta de misturar tipos brutos com genéricos, portanto, Enum<T extends Enum<T>>para consistência.
JAB

10

Vou lançar uma opinião realmente controversa. Os genéricos complicam a linguagem e complicam o código. Por exemplo, digamos que eu tenha um mapa que mapeia uma string para uma lista de strings. Nos velhos tempos, eu poderia declarar isso simplesmente como

Map someMap;

Agora, eu tenho que declará-lo como

Map<String, List<String>> someMap;

E cada vez que passo para algum método, tenho que repetir aquela longa declaração mais uma vez. Na minha opinião, toda aquela digitação extra distrai o desenvolvedor e o leva para fora da "zona". Além disso, quando o código é preenchido com muitos fragmentos, às vezes é difícil voltar a ele mais tarde e vasculhar rapidamente todos os fragmentos para encontrar a lógica importante.

Java já tem uma má reputação por ser uma das linguagens mais prolixas de uso comum, e os genéricos apenas aumentam esse problema.

E o que você realmente compra com toda essa verbosidade extra? Quantas vezes você realmente teve problemas quando alguém colocou um Integer em uma coleção que deveria conter Strings, ou quando alguém tentou extrair uma String de uma coleção de Inteiros? Em meus 10 anos de experiência trabalhando na construção de aplicativos Java comerciais, isso nunca foi uma grande fonte de erros. Portanto, não tenho certeza do que você está obtendo com a verbosidade extra. Isso realmente me parece uma bagagem burocrática extra.

Agora vou ficar muito controverso. O que vejo como o maior problema com as coleções em Java 1.4 é a necessidade de typecast em todos os lugares. Eu vejo essas previsões como detalhes extras e detalhados que têm muitos dos mesmos problemas que os genéricos. Então, por exemplo, não posso simplesmente fazer

List someList = someMap.get("some key");

eu tenho que fazer

List someList = (List) someMap.get("some key");

A razão, claro, é que get () retorna um Object que é um supertipo de List. Portanto, a atribuição não pode ser feita sem um typecast. Novamente, pense em quanto essa regra realmente compra você. Pela minha experiência, não muito.

Acho que Java teria ficado muito melhor se 1) não tivesse adicionado genéricos, mas 2) em vez disso, tivesse permitido a conversão implícita de um supertipo em um subtipo. Deixe que conversões incorretas sejam detectadas no tempo de execução. Então eu poderia ter a simplicidade de definir

Map someMap;

e depois fazendo

List someList = someMap.get("some key");

toda a sujeira teria sumido, e eu realmente não acho que introduziria uma grande nova fonte de bugs em meu código.


15
Não concordo com quase tudo que você disse. Mas não vou votar porque você argumenta bem.
Paul Tomblin

2
Claro, existem muitos exemplos de sistemas grandes e bem-sucedidos construídos em linguagens como Python e Ruby que fazem exatamente o que sugeri em minha resposta.
Clint Miller

Acho que toda a minha objeção a esse tipo de ideia não é que seja uma má ideia em si, mas sim que, como linguagens modernas como Python e Ruby tornam a vida cada vez mais fácil para os desenvolvedores, os desenvolvedores, por sua vez, ficam intelectualmente complacentes e, eventualmente, têm uma menor compreensão do seu próprio código.
Dan Tao

4
"E o que você realmente compra com toda essa verbosidade extra? ..." Diz ao pobre programador de manutenção o que deveria haver nessas coleções. Descobri que este é um problema ao trabalhar com aplicativos Java comerciais.
richj

4
"Quantas vezes você realmente teve problemas em que alguém colocava um [x] em uma coleção que deveria conter [y] s?" - Nossa, eu perdi a conta! Além disso, mesmo que não haja nenhum bug, é um assassino da legibilidade. E a varredura de muitos arquivos para descobrir o que o objeto vai ser (ou depuração) realmente me puxa para fora da zona. Você pode gostar de Haskell - até mesmo uma digitação forte, mas menos complicada (porque os tipos são inferidos).
Martin Capodici

8

Outro efeito colateral de serem em tempo de compilação e não em tempo de execução é que você não pode chamar o construtor do tipo genérico. Então você não pode usá-los para implementar uma fábrica genérica ...


   public class MyClass {
     public T getStuff() {
       return new T();
     }
    }

--jeffk ++


6

Os genéricos Java são verificados quanto à exatidão em tempo de compilação e, em seguida, todas as informações de tipo são removidas (o processo é chamado de eliminação de tipo . Assim, genérico List<Integer>será reduzido ao seu tipo bruto , não genérico List, que pode conter objetos de classe arbitrária.

Isso resulta na possibilidade de inserir objetos arbitrários na lista em tempo de execução, bem como agora é impossível dizer quais tipos foram usados ​​como parâmetros genéricos. Este último, por sua vez, resulta em

ArrayList<Integer> li = new ArrayList<Integer>();
ArrayList<Float> lf = new ArrayList<Float>();
if(li.getClass() == lf.getClass()) // evaluates to true
  System.out.println("Equal");

5

Eu gostaria que isso fosse um wiki para que eu pudesse adicionar a outras pessoas ... mas ...

Problemas:

  • Apagamento de tipo (sem disponibilidade de tempo de execução)
  • Sem suporte para tipos primários
  • Incompatibilidade com as anotações (ambas foram adicionadas no 1.5. Ainda não tenho certeza de por que as anotações não permitem genéricos além de acelerar os recursos)
  • Incompatibilidade com Arrays. (Às vezes, eu realmente quero fazer algo como Class <? Estende MyObject >[], mas não tenho permissão)
  • Sintaxe e comportamento curinga estranho
  • O fato de que o suporte genérico é inconsistente entre as classes Java. Eles o adicionaram à maioria dos métodos de coleção, mas de vez em quando, você encontra uma instância onde não está lá.

5

Ignorando toda a bagunça de eliminação de tipo, os genéricos especificados simplesmente não funcionam.

Isso compila:

List<Integer> x = Collections.emptyList();

Mas este é um erro de sintaxe:

foo(Collections.emptyList());

Onde foo é definido como:

void foo(List<Integer> x) { /* method body not important */ }

Portanto, se um tipo de expressão é verificado depende se ele está sendo atribuído a uma variável local ou a um parâmetro real de uma chamada de método. Quão louco é isso?


3
A inferência inconsistente por javac é uma porcaria.
mP.

3
Eu presumiria que a razão pela qual a última forma foi rejeitada é porque pode haver mais de uma versão de "foo" devido à sobrecarga do método.
Jason Creighton

2
esta crítica não se aplica mais porque o Java 8 introduziu inferência aprimorada de tipo de destino
oldrinb

4

A introdução de genéricos em Java foi uma tarefa difícil porque os arquitetos estavam tentando equilibrar funcionalidade, facilidade de uso e compatibilidade com versões anteriores do código legado. Como era de se esperar, acordos tiveram que ser feitos.

Alguns também acham que a implementação de genéricos em Java aumentou a complexidade da linguagem a um nível inaceitável (consulte " Generics Considered Harmful " de Ken Arnold ). As FAQs de Genéricos de Angelika Langer dão uma boa ideia de como as coisas podem se tornar complicadas.


3

Java não impõe Genéricos em tempo de execução, apenas em tempo de compilação.

Isso significa que você pode fazer coisas interessantes, como adicionar os tipos errados a coleções genéricas.


1
O compilador dirá que você errou se conseguir fazer isso.
Tom Hawtin - tackline

@Tom - não necessariamente. Existem maneiras de enganar o compilador.
Paul Tomblin

2
Você não escuta o que o compilador lhe diz ?? (veja meu primeiro comentário)
Tom Hawtin - tackline

1
@Tom, se você tiver um ArrayList <String> e o passar para um método antigo (digamos, código legado ou de terceiros) que recebe uma Lista simples, esse método pode adicionar qualquer coisa que desejar a essa Lista. Se for aplicado em tempo de execução, você obterá uma exceção.
Paul Tomblin

1
static void addToList (List list) {list.add (1); } List <String> list = new ArrayList <String> (); addToList (lista); Isso compila e até funciona em tempo de execução. A única vez que você vê um problema é quando remove da lista esperando uma string e obtém um int.
Laplie Anderson

3

Os genéricos Java são apenas em tempo de compilação e são compilados em código não genérico. Em C #, o MSIL compilado real é genérico. Isso tem grandes implicações para o desempenho porque o Java ainda faz projeções durante o tempo de execução. Veja aqui para mais informações .


2

Se você ouvir Java Posse # 279 - Entrevista com Joe Darcy e Alex Buckley , eles falam sobre esse assunto. Isso também leva a uma postagem do blog Neal Gafter intitulada Reified Generics for Java que diz:

Muitas pessoas estão insatisfeitas com as restrições causadas pela maneira como os genéricos são implementados em Java. Especificamente, eles estão descontentes com o fato de os parâmetros de tipo genérico não serem reificados: eles não estão disponíveis no tempo de execução. Os genéricos são implementados usando erasure, em que os parâmetros de tipo genérico são simplesmente removidos no tempo de execução.

Essa postagem do blog faz referência a uma entrada mais antiga, Puzzling Through Erasure: seção de respostas , que enfatizou o ponto sobre a compatibilidade da migração nos requisitos.

O objetivo era fornecer compatibilidade com versões anteriores de código-fonte e código-objeto, e também compatibilidade de migração.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.