Eu li um tweet hoje que dizia:
É engraçado quando os usuários Java reclamam do apagamento de tipo, que é a única coisa que o Java acertou, enquanto ignoram todas as coisas erradas.
Portanto, minha pergunta é:
Existem benefícios da eliminação de tipo do Java? Quais são os benefícios técnicos ou de estilo de programação que ele (possivelmente) oferece, além da preferência de implementações JVM para compatibilidade com versões anteriores e desempenho de tempo de execução?
java
type-erasure
Verti
fonte
fonte
Respostas:
Apagamento de tipo é bom
Vamos nos ater aos fatos
Muitas das respostas até agora estão excessivamente preocupadas com o usuário do Twitter. É útil manter o foco nas mensagens e não no mensageiro. Há uma mensagem bastante consistente mesmo apenas com os trechos mencionados até agora:
Uma meta: programas razoáveis
Esses tweets refletem uma perspectiva que não está interessada em saber se podemos fazer a máquina fazer algo , mas mais se podemos raciocinar que a máquina fará algo que realmente queremos. O bom raciocínio é uma prova. As provas podem ser especificadas em notação formal ou algo menos formal. Independentemente da linguagem de especificação, eles devem ser claros e rigorosos. As especificações informais não são impossíveis de estruturar corretamente, mas costumam apresentar falhas na programação prática. Acabamos com remediações como testes automatizados e exploratórios para compensar os problemas que temos com o raciocínio informal. Isso não quer dizer que o teste seja intrinsecamente uma má ideia, mas o usuário do Twitter citado está sugerindo que existe uma maneira muito melhor.
Portanto, nosso objetivo é ter programas corretos sobre os quais possamos raciocinar de forma clara e rigorosa de uma forma que corresponda a como a máquina realmente executará o programa. Este, porém, não é o único objetivo. Também queremos que nossa lógica tenha um certo grau de expressividade. Por exemplo, há muito que podemos expressar com a lógica proposicional. É bom ter quantificação universal (∀) e existencial (∃) de algo como lógica de primeira ordem.
Usando sistemas de tipo para raciocinar
Esses objetivos podem ser muito bem tratados por sistemas de tipos. Isso é especialmente claro por causa da correspondência Curry-Howard . Essa correspondência é freqüentemente expressa com a seguinte analogia: os tipos estão para os programas assim como os teoremas estão para as provas.
Essa correspondência é um tanto profunda. Podemos pegar expressões lógicas e traduzi-las por meio da correspondência de tipos. Então, se temos um programa com a mesma assinatura de tipo que compila, provamos que a expressão lógica é universalmente verdadeira (uma tautologia). Isso ocorre porque a correspondência é bidirecional. A transformação entre o tipo / programa e os mundos do teorema / prova é mecânica e pode, em muitos casos, ser automatizada.
Curry-Howard joga bem com o que gostaríamos de fazer com as especificações de um programa.
Os sistemas de tipos são úteis em Java?
Mesmo com uma compreensão de Curry-Howard, algumas pessoas acham fácil descartar o valor de um sistema de tipos, quando
Com relação ao primeiro ponto, talvez os IDEs tornem o sistema de tipos Java fácil de trabalhar (isso é altamente subjetivo).
Em relação ao segundo ponto, Java passa a corresponder quase a uma lógica de primeira ordem. Os genéricos fornecem o equivalente ao sistema de tipos da quantificação universal. Infelizmente, os curingas nos fornecem apenas uma pequena fração da quantificação existencial. Mas a quantificação universal é um bom começo. É bom poder dizer que funciona
List<A>
universalmente para todas as listas possíveis porque A é completamente irrestrito. Isso leva ao que o usuário do Twitter está falando com respeito à "parametricidade".Um artigo frequentemente citado sobre parametricidade são os Teoremas de Philip Wadler gratuitamente! . O que é interessante sobre este artigo é que apenas com a assinatura de tipo sozinha, podemos provar alguns invariantes muito interessantes. Se tivéssemos que escrever testes automatizados para essas invariantes, estaríamos perdendo muito nosso tempo. Por exemplo, para
List<A>
, da assinatura de tipo sozinha paraflatten
podemos raciocinar que
Este é um exemplo simples, e você provavelmente pode raciocinar sobre isso informalmente, mas é ainda mais agradável quando obtemos essas provas formalmente de graça no sistema de tipos e verificadas pelo compilador.
Não apagar pode levar a abusos
Do ponto de vista da implementação da linguagem, os genéricos de Java (que correspondem aos tipos universais) jogam fortemente na parametricidade usada para obter provas sobre o que nossos programas fazem. Isso leva ao terceiro problema mencionado. Todos esses ganhos de prova e correção requerem um sistema de tipo de som implementado sem defeitos. Java definitivamente tem alguns recursos de linguagem que nos permitem quebrar nosso raciocínio. Estes incluem, mas não estão limitados a:
Os genéricos não apagados estão, de muitas maneiras, relacionados à reflexão. Sem exclusão, há informações de tempo de execução que são transportadas com a implementação e que podemos usar para projetar nossos algoritmos. O que isso significa é que, estaticamente, quando raciocinamos sobre programas, não temos o quadro completo. A reflexão ameaça severamente a correção de quaisquer provas sobre as quais raciocinamos estaticamente. Não é por acaso que a reflexão também leva a uma variedade de defeitos complicados.
Então, de que maneiras os genéricos não apagados podem ser "úteis"? Vamos considerar o uso mencionado no tweet:
O que acontece se T não tiver um construtor sem arg? Em alguns idiomas, o que você obtém é nulo. Ou talvez você ignore o valor nulo e vá direto para o lançamento de uma exceção (à qual os valores nulos parecem levar de qualquer maneira). Como nossa linguagem é Turing completa, é impossível raciocinar sobre quais chamadas para
broken
envolverão tipos "seguros" com construtores sem arg e quais não. Perdemos a certeza de que nosso programa funciona universalmente.Apagar significa que raciocinamos (então vamos apagar)
Portanto, se quisermos raciocinar sobre nossos programas, somos fortemente aconselhados a não empregar recursos de linguagem que ameacem fortemente nosso raciocínio. Depois de fazer isso, por que não simplesmente descartar os tipos em tempo de execução? Eles não são necessários. Podemos obter alguma eficiência e simplicidade com a satisfação de que nenhuma conversão falhará ou que métodos podem estar faltando na invocação.
Apagar incentiva o raciocínio.
fonte
Tipos são construções usadas para escrever programas de uma maneira que permite ao compilador verificar a exatidão de um programa. Um tipo é uma proposição sobre um valor - o compilador verifica se essa proposição é verdadeira.
Durante a execução de um programa, não deve haver necessidade de informações de tipo - isso já foi verificado pelo compilador. O compilador deve estar livre para descartar essas informações a fim de realizar otimizações no código - torná-lo mais rápido, gerar um binário menor etc. O apagamento dos parâmetros de tipo facilita isso.
Java interrompe a tipagem estática permitindo que as informações de tipo sejam consultadas em tempo de execução - reflexão, instância de etc. Isso permite que você construa programas que não podem ser verificados estaticamente - eles ignoram o sistema de tipos. Também perde oportunidades de otimização estática.
O fato de que os parâmetros de tipo são apagados evita que algumas instâncias desses programas incorretos sejam construídos; no entanto, programas mais incorretos não seriam permitidos se mais informações de tipo fossem apagadas e os recursos de reflexão e instância de instância fossem removidos.
O apagamento é importante para manter a propriedade de "parametricidade" de um tipo de dados. Digamos que eu tenha um tipo "Lista" parametrizado sobre o tipo de componente T. ou seja, Lista <T>. Esse tipo é uma proposição de que esse tipo de Lista funciona de maneira idêntica para qualquer tipo T. O fato de T ser um parâmetro de tipo abstrato e ilimitado significa que não sabemos nada sobre esse tipo, portanto, somos impedidos de fazer qualquer coisa especial para casos especiais de T.
por exemplo, diga que tenho uma Lista xs = asList ("3"). Eu adiciono um elemento: xs.add ("q"). Acabo com ["3", "q"]. Como isso é paramétrico, posso assumir que List xs = asList (7); xs.add (8) termina com [7,8]. Eu sei pelo tipo que ele não faz uma coisa para String e outra para Int.
Além disso, eu sei que a função List.add não pode inventar valores de T do nada. Eu sei que se minha asList ("3") tiver um "7" adicionado a ela, as únicas respostas possíveis seriam construídas com os valores "3" e "7". Não há possibilidade de um "2" ou "z" ser adicionado à lista porque a função não seria capaz de construí-la. Nenhum desses outros valores seria sensato adicionar, e a parametricidade impede que esses programas incorretos sejam construídos.
Basicamente, o apagamento impede alguns meios de violar a parametricidade, eliminando assim possibilidades de programas incorretos, que é o objetivo da digitação estática.
fonte
(Embora eu já tenha escrito uma resposta aqui, revisitando esta questão dois anos depois, percebi que existe uma outra maneira completamente diferente de respondê-la, então estou deixando a resposta anterior intacta e adicionando esta.)
É altamente discutível se o processo feito em Java Generics merece o nome de "eliminação de tipo". Uma vez que os tipos genéricos não são apagados, mas substituídos por suas contrapartes brutas, uma escolha melhor parece ser "mutilação de tipo".
A característica quintessencial do apagamento de tipo em seu sentido comumente compreendido é forçar o tempo de execução a permanecer dentro dos limites do sistema de tipo estático, tornando-o "cego" para a estrutura dos dados que acessa. Isso dá força total ao compilador e permite que ele prove teoremas baseados apenas em tipos estáticos. Também ajuda o programador restringindo os graus de liberdade do código, dando mais poder ao raciocínio simples.
O apagamento de tipo do Java não consegue isso - ele paralisa o compilador, como neste exemplo:
(As duas declarações acima se resumem na mesma assinatura de método após o apagamento.)
Por outro lado, o tempo de execução ainda pode inspecionar o tipo de um objeto e raciocinar sobre ele, mas como seu insight sobre o tipo verdadeiro é prejudicado pelo apagamento, as violações de tipo estático são triviais de se obter e difíceis de prevenir.
Para tornar as coisas ainda mais complicadas, as assinaturas de tipo original e apagada coexistem e são consideradas em paralelo durante a compilação. Isso ocorre porque todo o processo não é para remover informações de tipo do tempo de execução, mas sobre colocar um sistema de tipo genérico em um sistema de tipo bruto legado para manter a compatibilidade com versões anteriores. Esta joia é um exemplo clássico:
(O redundante
extends Object
teve que ser adicionado para preservar a compatibilidade com versões anteriores da assinatura apagada.)Agora, com isso em mente, vamos revisitar a citação:
O que exatamente o Java acertou? É a própria palavra, independentemente do significado? Para contraste, dê uma olhada no
int
tipo humilde : nenhuma verificação de tipo em tempo de execução é executada, ou mesmo possível, e a execução é sempre perfeitamente segura para o tipo. Essa é a aparência do apagamento de tipo quando feito da maneira certa: você nem sabe que está lá.fonte
A única coisa que não vejo considerado aqui é que o polimorfismo de tempo de execução OOP é fundamentalmente dependente da reificação de tipos em tempo de execução. Quando uma linguagem cuja espinha dorsal é mantida no lugar por tipos refinados introduz uma extensão principal ao seu sistema de tipos e se baseia na eliminação de tipos, a dissonância cognitiva é o resultado inevitável. Isso é exatamente o que aconteceu com a comunidade Java; é por isso que o apagamento de tipo atraiu tanta controvérsia e, em última análise, por que existem planos para desfazê-lo em uma versão futura do Java . Encontrar algo engraçado nessa reclamação dos usuários de Java denuncia um mal-entendido honesto do espírito de Java ou uma piada conscientemente depreciativa.
A afirmação "exclusão é a única coisa que Java acertou" implica a afirmação de que "todas as linguagens baseadas em despacho dinâmico contra o tipo de argumento de função em tempo de execução são fundamentalmente falhos". Embora certamente uma afirmação legítima por si só, e que pode até ser considerada uma crítica válida de todas as linguagens OOP, incluindo Java, não pode se alojar como um ponto central a partir do qual avaliar e criticar recursos dentro do contexto de Java , onde o polimorfismo do tempo de execução é axiomático.
Em resumo, embora se possa afirmar com validade "a eliminação de tipo é o caminho a percorrer no design de linguagem", as posições que suportam a eliminação de tipo dentro de Java são deslocadas simplesmente porque é tarde demais para isso e já tinha sido até mesmo no momento histórico quando Oak foi abraçado pela Sun e renomeado para Java.
Quanto ao fato de a própria tipagem estática ser a direção apropriada no projeto de linguagens de programação, isso se encaixa em um contexto filosófico muito mais amplo do que pensamos constituir a atividade de programação . Uma escola de pensamento, claramente derivada da tradição clássica da matemática, vê os programas como instâncias de um conceito matemático ou outro (proposições, funções, etc.), mas há uma classe totalmente diferente de abordagens, que vêem a programação como uma forma de fale com a máquina e explique o que queremos dela. Nessa visão, o programa é uma entidade dinâmica e de crescimento orgânico, um oposto dramático do aedifício cuidadosamente erguido de um programa estaticamente tipado.
Pareceria natural considerar as linguagens dinâmicas um passo nessa direção: a consistência do programa surge de baixo para cima, sem constrangimentos a priori que a imponham de cima para baixo. Esse paradigma pode ser visto como um passo em direção à modelagem do processo pelo qual nós, humanos, nos tornamos o que somos por meio do desenvolvimento e do aprendizado.
fonte
Uma postagem subsequente do mesmo usuário na mesma conversa:
(Isso foi em resposta a uma declaração de outro usuário, a saber, que "parece que em algumas situações 'novo T' seria melhor", a ideia de que
new T()
é impossível devido ao apagamento de tipo. (Isso é discutível - mesmo seT
estivesse disponível em runtime, pode ser uma classe ou interface abstrata, ou pode serVoid
, ou pode não ter um construtor no-arg, ou seu construtor no-arg pode ser privado (por exemplo, porque é suposto ser uma classe singleton), ou seu O construtor no-arg poderia especificar uma exceção verificada que o método genérico não detecta ou especifica - mas essa era a premissa. Independentemente disso, é verdade que, sem a eliminação, você poderia pelo menos escreverT.class.newInstance()
, o que lida com esses problemas.))Essa visão, de que os tipos são isomórficos às proposições, sugere que o usuário tem experiência na teoria formal dos tipos. (S) ele muito provavelmente não gosta de "tipos dinâmicos" ou "tipos de tempo de execução" e prefere um Java sem downcasts
instanceof
e reflexão e assim por diante. (Pense em uma linguagem como Standard ML, que tem um sistema de tipos muito rico (estático) e cuja semântica dinâmica não depende de nenhuma informação de tipo.Vale a pena ter em mente, a propósito, que o usuário está trollando: enquanto (s) ele provavelmente prefere sinceramente linguagens digitadas (estaticamente), ele não está sinceramente tentando persuadir os outros dessa visão. Em vez disso, o objetivo principal do tweet original era zombar daqueles que discordam e, depois que alguns desses discordantes entraram na conversa, o usuário postou tweets de acompanhamento, como "a razão de java ter apagamento de tipo é que Wadler e outros sabem o que eles estão fazendo, ao contrário dos usuários de java ". Infelizmente, isso torna difícil descobrir o que ele está realmente pensando; mas, felizmente, provavelmente também significa que não é muito importante fazer isso. Pessoas com profundidade real em suas opiniões geralmente não recorrem a trolls que são totalmente livres de conteúdo.
fonte
Uma coisa boa é que não houve necessidade de alterar a JVM quando os genéricos foram introduzidos. Java implementa genéricos apenas no nível do compilador.
fonte
A razão pela qual o apagamento de tipo é uma coisa boa é que as coisas que ela torna impossível são prejudiciais. Impedir a inspeção de argumentos de tipo em tempo de execução facilita a compreensão e raciocínio sobre os programas.
Uma observação que achei um tanto contra-intuitiva é que, quando as assinaturas de função são mais genéricas, elas se tornam mais fáceis de entender. Isso ocorre porque o número de implementações possíveis é reduzido. Considere um método com esta assinatura, que de alguma forma sabemos que não tem efeitos colaterais:
Quais são as possíveis implementações desta função? Muitos. Você pode dizer muito pouco sobre o que essa função faz. Pode estar revertendo a lista de entrada. Pode ser emparelhar ints, somar e retornar uma lista com metade do tamanho. Existem muitas outras possibilidades que poderiam ser imaginadas. Agora considere:
Quantas implementações desta função existem? Uma vez que a implementação não pode saber o tipo dos elementos, um grande número de implementações pode agora ser excluído: os elementos não podem ser combinados, ou adicionados à lista ou filtrados, et al. Estamos limitados a coisas como: identidade (sem alteração na lista), descartar elementos ou inverter a lista. Esta função é mais fácil de raciocinar com base apenas em sua assinatura.
Exceto ... em Java você sempre pode enganar o sistema de tipos. Como a implementação desse método genérico pode usar coisas como
instanceof
verificações e / ou conversões para tipos arbitrários, nosso raciocínio baseado na assinatura de tipo pode ser facilmente tornado inútil. A função pode inspecionar o tipo dos elementos e fazer uma série de coisas com base no resultado. Se esses hacks de tempo de execução forem permitidos, as assinaturas de método parametrizadas se tornarão muito menos úteis para nós.Se o Java não tivesse apagamento de tipo (ou seja, os argumentos de tipo fossem reificados em tempo de execução), isso simplesmente permitiria mais travessuras desse tipo que prejudicam o raciocínio. No exemplo acima, a implementação só pode violar as expectativas definidas pela assinatura de tipo se a lista tiver pelo menos um elemento; mas se
T
fosse reificado, poderia fazê-lo mesmo se a lista estivesse vazia. Os tipos reificados apenas aumentariam as (já muitas) possibilidades de impedir nossa compreensão do código.A eliminação do tipo torna a linguagem menos "poderosa". Mas algumas formas de "poder" são realmente prejudiciais.
fonte
instanceof
impedem nossa capacidade de raciocinar sobre o que o código faz com base em tipos. Se o Java reificasse os argumentos de tipo, só pioraria o problema. Apagar tipos em tempo de execução tem o efeito de tornar o sistema de tipos mais útil.Esta não é uma resposta direta (OP perguntou "quais são os benefícios", estou respondendo "quais são os contras")
Comparado ao sistema de tipo C #, o apagamento de tipo Java é uma dor real para dois raesons
Você não pode implementar uma interface duas vezes
Em C #, você pode implementar ambos
IEnumerable<T1>
eIEnumerable<T2>
com segurança, especialmente se os dois tipos não compartilham um ancestral comum (ou seja, seu ancestral éObject
).Exemplo prático: no Spring Framework, você não pode implementar
ApplicationListener<? extends ApplicationEvent>
várias vezes. Se você precisa de comportamentos diferentes com base em,T
você precisa testarinstanceof
Você não pode fazer novo T ()
(e você precisa de uma referência à classe para fazer isso)
Como outros comentaram, fazer o equivalente de
new T()
só pode ser feito por meio de reflexão, apenas invocando uma instância deClass<T>
, certificando-se dos parâmetros exigidos pelo construtor. C # permite que você façanew T()
apenas se você restringirT
ao construtor sem parâmetros. SeT
não respeitar essa restrição, será gerado um erro de compilação .Em Java, você frequentemente será forçado a escrever métodos semelhantes aos seguintes
As desvantagens do código acima são:
ReflectiveOperationException
é lançado em tempo de execuçãoSe eu fosse o autor do C #, teria introduzido a capacidade de especificar uma ou mais restrições de construtor que são fáceis de verificar em tempo de compilação (portanto, posso exigir, por exemplo, um construtor com
string,string
parâmetros). Mas o último é especulaçãofonte
Um ponto adicional que nenhuma das outras respostas parece ter considerado: se você realmente precisa de genéricos com tipagem em tempo de execução , você mesmo pode implementá-lo desta forma:
Esta classe é então capaz de fazer todas as coisas que seriam alcançáveis por padrão se o Java não usasse erasure: ela pode alocar novos
T
s (assumindo queT
tenha um construtor que corresponda ao padrão que espera usar), ou matrizes deT
s, pode teste dinamicamente em tempo de execução se um determinado objeto é umT
e mude o comportamento dependendo disso, e assim por diante.Por exemplo:
fonte
evita o inchaço de código semelhante ao c ++ porque o mesmo código é usado para vários tipos; no entanto, o apagamento de tipo requer envio virtual, enquanto a abordagem c ++ - inchaço de código pode fazer genéricos não despachados virtualmente
fonte
A maioria das respostas está mais preocupada com a filosofia de programação do que com os detalhes técnicos reais.
E embora essa pergunta tenha mais de 5 anos, a pergunta ainda persiste: Por que a eliminação de tipo é desejável do ponto de vista técnico? No final, a resposta é bastante simples (em um nível superior): https://en.wikipedia.org/wiki/Type_erasure
Os modelos C ++ não existem em tempo de execução. O compilador emite uma versão totalmente otimizada para cada invocação, o que significa que a execução não depende das informações de tipo. Mas como um JIT lida com diferentes versões da mesma função? Não seria melhor ter apenas uma função? Não gostaria que o JIT tivesse que otimizar todas as diferentes versões dele. Bem, mas e quanto à segurança de tipo? Acho que isso tem que sair pela janela.
Mas espere um segundo: como o .NET faz isso? Reflexão! Dessa forma, eles só precisam otimizar uma função e também obter informações sobre o tipo de tempo de execução. E é por isso que os genéricos .NET costumavam ser mais lentos (embora tenham ficado muito melhores). Não estou argumentando que isso não seja conveniente! Mas é caro e não deve ser usado quando não for absolutamente necessário (não é considerado caro em linguagens tipadas dinamicamente porque o compilador / interpretador depende da reflexão de qualquer maneira).
Desta forma, a programação genérica com eliminação de tipo é quase zero (algumas verificações / conversões de tempo de execução ainda são necessárias): https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
fonte