Comparação de C Sharp e Java - Comparison of C Sharp and Java

Este artigo compara duas linguagens de programação : C # com Java . Embora o foco deste artigo seja principalmente as linguagens e seus recursos, tal comparação necessariamente considerará alguns recursos de plataformas e bibliotecas . Para uma comparação mais detalhada das plataformas, consulte Comparação das plataformas Java e .NET .

C # e Java são linguagens semelhantes que são digitadas estática, forte e manifestamente . Ambos são orientados a objeto , e projetado com semi- interpretação ou execução compilação just-in-time , e ambas são línguas chaveta , como C e C ++ .

Tipos

Tipos de dados Java C #
Decimais de tamanho arbitrário Tipo de referência; sem operadores Biblioteca de terceiros
Inteiros de tamanho arbitrário Tipo de referência; sem operadores sim
Matrizes sim sim
Tipo booleano sim sim
Personagem sim sim
Números complexos Biblioteca de terceiros sim
Data hora Sim; tipo de referência Sim; tipo de valor
Tipos enumerados Sim; tipo de referência Sim; escalar
Número decimal de alta precisão Não; mas veja 'Decimais de tamanho arbitrário' acima 128 bits (28 dígitos) Tipo decimal
Número de ponto flutuante binário IEEE 754 32 sim sim
Número de ponto flutuante binário IEEE 754 64 sim sim
Tipos levantados (anuláveis) Não; mas tipos de invólucro sim
Ponteiros Não; apenas referências de método sim
Tipos de referência sim sim
Inteiros assinados Sim; 8, 16, 32, 64 bits Sim; 8, 16, 32, 64 bits
Cordas Tipo de referência imutável, Unicode Tipo de referência imutável, Unicode
Anotações de tipo sim sim
Sistema de tipo de raiz única (unificado) Não; mas tipos de invólucro sim
Tuplas Não; disponível de terceiros limitados. sim
Inteiros sem sinal Não; mas algum suporte de método. Sim; 8, 16, 32, 64 bits
Tipos de valor Não; apenas tipos primitivos sim

Sistema de tipo unificado

Ambas as linguagens são tipadas estaticamente com orientação a objetos baseada em classe. Em Java, os tipos primitivos são especiais porque não são orientados a objetos e não podem ter sido definidos usando a própria linguagem. Eles também não compartilham um ancestral comum com tipos de referência. Todos os tipos de referência Java derivam de um tipo de raiz comum. C # tem um sistema de tipo unificado no qual todos os tipos (exceto os ponteiros não seguros) derivam de um tipo de raiz comum. Consequentemente, todos os tipos implementam os métodos desse tipo de raiz e os métodos de extensão definidos para o objecttipo se aplicam a todos os tipos, até mesmo intliterais primitivos e delegados . Isso permite que o C #, ao contrário do Java, ofereça suporte a objetos com encapsulamento que não são tipos de referência.

Em Java, os tipos compostos são sinônimos de tipos de referência; os métodos não podem ser definidos para um tipo, a menos que também seja um tipo de referência de classe . Em C #, os conceitos de encapsulamento e métodos foram separados do requisito de referência para que um tipo possa oferecer suporte a métodos e encapsulamento sem ser um tipo de referência. No entanto, apenas os tipos de referência oferecem suporte a métodos virtuais e especialização.

Ambas as linguagens oferecem suporte a muitos tipos integrados que são copiados e passados ​​por valor, e não por referência. Java chama esses tipos de tipos primitivos , enquanto eles são chamados de tipos simples em C #. Os tipos primitivos / simples geralmente têm suporte nativo da arquitetura do processador subjacente.

Os tipos simples C # implementam várias interfaces e, conseqüentemente, oferecem muitos métodos diretamente nas instâncias dos tipos, até mesmo nos literais. Os nomes de tipo C # também são meramente apelidos para tipos Common Language Runtime (CLR). O System.Int64tipo C # é exatamente o mesmo tipo que o longtipo; a única diferença é que o primeiro é o nome canônico do .NET, enquanto o último é um alias C # para ele.

Java não oferece métodos diretamente em tipos primitivos. Em vez disso, os métodos que operam em valores primitivos são oferecidos por meio de classes de wrapper primitivas companheiras . Existe um conjunto fixo dessas classes de wrapper, cada uma delas envolvendo um dos conjuntos fixos de tipos primitivos. Por exemplo, o Longtipo Java é um tipo de referência que envolve o longtipo primitivo . Eles não são do mesmo tipo, no entanto.

Tipos de dados

Tipos numéricos

Inteiros assinados

Java e C # suportam inteiros assinados com larguras de bits de 8, 16, 32 e 64 bits. Eles usam o mesmo nome / aliases para os tipos, exceto para o inteiro de 8 bits que é chamado de byteem Java e de sbyte(byte assinado) em C #.

Inteiros sem sinal

C # oferece suporte a não assinado , além dos tipos inteiros assinados . Os tipos não assinados são byte, ushort, uinte ulongpor 8, 16, 32 e 64 larguras de bits, respectivamente. Também há suporte para operações aritméticas não assinadas nos tipos. Por exemplo, adicionar dois inteiros sem sinal uintainda produz a uintcomo resultado; não é um inteiro longo ou assinado.

Java não possui tipos inteiros sem sinal. Em particular, Java não possui um tipo primitivo para um byte não assinado . Em vez disso, o bytetipo de Java é estendido por sinal , que é uma fonte comum de bugs e confusão.

Inteiros sem sinal foram deixados de fora de Java deliberadamente porque James Gosling acreditava que os programadores não entenderiam como a aritmética sem sinal funciona.

No projeto de uma linguagem de programação, um dos problemas padrão é que a linguagem se torna tão complexa que ninguém consegue entendê-la. Um dos pequenos experimentos que fiz foi perguntar às pessoas sobre as regras da aritmética sem sinal em C. Acontece que ninguém entende como a aritmética sem sinal em C funciona. Existem algumas coisas óbvias que as pessoas entendem, mas muitas pessoas não entendem.

Números decimais de alta precisão

C # tem um tipo e notação literal para aritmética decimal de alta precisão (28 dígitos decimais) que é apropriada para cálculos financeiros e monetários. Ao contrário dos tipos de dados floate double, os números fracionários decimais como 0,1 podem ser representados exatamente na representação decimal. Nas representações flutuantes e duplas, esses números costumam ter expansões binárias não terminais, tornando essas representações mais sujeitas a erros de arredondamento.

Embora Java não tenha esse tipo integrado, a biblioteca Java apresenta um tipo decimal de precisão arbitrária . Este não é considerado um tipo de linguagem e não oferece suporte aos operadores aritméticos usuais; em vez disso, é um tipo de referência que deve ser manipulado usando os métodos de tipo. Veja mais sobre números de tamanho / precisão arbitrários abaixo .

Tipos numéricos avançados

Ambas as linguagens oferecem tipos aritméticos de precisão arbitrária definidos por biblioteca para inteiros de tamanho arbitrário e cálculos de ponto decimal.

Apenas Java possui um tipo de dados para cálculos de casas decimais de precisão arbitrária. Apenas C # tem um tipo para trabalhar com números complexos .

Em ambos os idiomas, o número de operações que podem ser realizadas nos tipos numéricos avançados é limitado em comparação com os tipos de ponto flutuante IEEE 754 integrados . Por exemplo, nenhum dos tipos de tamanho arbitrário suporta raiz quadrada ou logaritmos .

C # permite que os tipos definidos pela biblioteca sejam integrados a tipos e operadores existentes usando conversões implícitas / explícitas personalizadas e sobrecarga de operador. Veja o exemplo na seção Integração de tipos definidos pela biblioteca

Personagens

Ambas as linguagens apresentam um chartipo de dados nativo (caractere) como um tipo simples. Embora o chartipo possa ser usado com operadores bit a bit, isso é realizado promovendo o charvalor a um valor inteiro antes da operação. Portanto, o resultado de uma operação bit a bit é um tipo numérico, não um caractere, em ambos os idiomas.

Tipos de dados compostos integrados

Ambas as linguagens tratam strings como objetos ( imutáveis ) de tipo de referência. Em ambas as linguagens, o tipo contém vários métodos para manipular strings, analisar, formatar, etc. Em ambas as linguagens, as expressões regulares são consideradas um recurso externo e são implementadas em classes separadas.

As bibliotecas de ambas as línguas definem classes para trabalhar com datas e calendários em diferentes culturas. O Java java.util.Dateé um tipo de referência mutável, enquanto o C # System.DateTimeé um tipo de valor de estrutura. C # adicionalmente define um TimeSpantipo para trabalhar com períodos de tempo. Ambos os idiomas suportam aritmética de data e hora de acordo com as diferentes culturas.

Tipo de valor definido pelo usuário (estrutura)

C # permite que o programador crie tipos de valor definidos pelo usuário , usando a structpalavra - chave. Ao contrário das classes e como os primitivos padrão, esses tipos de valor são passados ​​e atribuídos por valor, e não por referência. Eles também podem fazer parte de um objeto (como um campo ou em uma caixa ), ou armazenados em uma matriz sem o engano de memória que normalmente existe para tipos de classe.

Como os tipos de valor não têm noção de um nullvalor e podem ser usados ​​em arrays sem inicialização, eles sempre vêm com um construtor padrão implícito que essencialmente preenche o espaço de memória da estrutura com zeros. O programador só pode definir construtores adicionais com um ou mais argumentos. Os tipos de valor não têm tabelas de métodos virtuais e, por causa disso (e da área de cobertura de memória fixa), são implicitamente lacrados. No entanto, os tipos de valor podem (e freqüentemente fazem) implementar interfaces . Por exemplo, os tipos inteiros integrados implementam várias interfaces .

Além dos tipos primitivos integrados, Java não inclui o conceito de tipos de valor.

Enumerações

Ambas as linguagens definem enumerações, mas elas são implementadas de maneiras fundamentalmente diferentes. Assim, as enumerações são uma área em que as ferramentas projetadas para traduzir automaticamente o código entre as duas linguagens (como conversores de Java para C #) falham.

C # implementou enumerações de maneira semelhante a C, ou seja, como invólucros em torno dos sinalizadores de bits implementados em tipos integrais primitivos (int, byte, short, etc.). Isso traz benefícios de desempenho e melhora a interação com o código compilado C / C ++, mas fornece menos recursos e pode levar a bugs se os tipos de valor de baixo nível forem diretamente convertidos em um tipo de enumeração, como é permitido na linguagem C #. Portanto, é visto como açúcar sintático . Em contraste, Java implementa enumerações como uma coleção completa de instâncias, exigindo mais memória e não auxiliando na interação com o código C / C ++, mas fornecendo recursos adicionais de reflexão e comportamento intrínseco. A implementação em cada linguagem é descrita na tabela abaixo.

Java C #
Definição Em Java, o tipo de enumeração é uma classe e seus valores são objetos (instâncias) dessa classe. Os únicos valores válidos são aqueles listados na enumeração. O tipo de enumeração pode declarar campos , permitindo que cada valor enumerado individual faça referência a dados adicionais associados exclusivamente a esse valor específico. O tipo de enumeração também pode declarar ou substituir métodos ou implementar interfaces . Enumerações em C # são implicitamente derivadas do Enumtipo que, novamente, é uma derivada de tipo de valor. O conjunto de valores de uma enumeração C # é definido pelo tipo subjacente que pode ser um tipo inteiro assinado ou não assinado de 8, 16, 32 ou 64 bits. A definição de enumeração define nomes para os valores inteiros selecionados. Por padrão, o valor 0 (zero) é atribuído ao primeiro nome e os nomes a seguir são atribuídos em incrementos de 1. Qualquer valor do tipo primitivo subjacente é um valor válido do tipo de enumeração, embora uma conversão explícita possa ser necessária para atribuí-lo .
Combinando O conjunto de enumeração Java e as coleções de mapas fornecem funcionalidade para combinar vários valores de enumeração em um valor combinado. Essas coleções especiais permitem a otimização do compilador para minimizar a sobrecarga incorrida pelo uso de coleções como o mecanismo de combinação. OC # oferece suporte a enumerações mapeadas em bits, em que um valor real pode ser uma combinação de valores enumerados em bits ou juntos. Os métodos de formatação e análise implicitamente definidos pelo tipo tentarão usar esses valores.

Em C # e Java, os programadores podem usar enumerações em uma instrução switch sem conversão para uma string ou tipo inteiro primitivo. No entanto, C # não permite falhas implícitas, a menos que a instrução case não contenha nenhum código, pois é uma causa comum de bugs difíceis de encontrar. Fall-through deve ser declarado explicitamente usando uma instrução goto.

Delegados, referências de método

C # implementa ponteiros de métodos orientados a objetos na forma de delegados . Um delegado é um tipo especial que pode capturar uma referência a um método. Essa referência pode então ser armazenada em uma variável do tipo delegado ou passada para um método por meio de um parâmetro delegado para chamada posterior. Os delegados C # suportam covariância e contravariância e podem conter uma referência a qualquer método estático compatível com assinatura, método de instância, método anônimo ou expressão lambda .

Delegados não devem ser confundidos com fechamentos e funções embutidas. Os conceitos estão relacionados porque uma referência a uma função closure / inline deve ser capturada em uma referência delegada para ser útil. Mas um delegado nem sempre faz referência a uma função embutida; ele também pode fazer referência a métodos estáticos ou de instância existentes. Os delegados formam a base dos eventos C # , mas também não devem ser confundidos com eles.

Os delegados foram deliberadamente deixados de fora do Java porque foram considerados desnecessários e prejudiciais à linguagem e por causa de possíveis problemas de desempenho. Em vez disso, mecanismos alternativos são usados. O padrão de wrapper , que se assemelha aos delegados do C # no sentido de que permite ao cliente acessar um ou mais métodos definidos pelo cliente por meio de uma interface conhecida , é um desses mecanismos. Outro é o uso de objetos adaptadores usando classes internas, que os designers de Java argumentaram ser uma solução melhor do que referências de método vinculadas.

Veja também delegados C # de exemplo e construções Java equivalentes .

Tipos levantados (anuláveis)

C # permite que tipos de valor / primitivos / simples sejam "elevados" para permitir o nullvalor especial além dos valores nativos do tipo. Um tipo é levantado adicionando um ?sufixo ao nome do tipo; isso é equivalente a usar o tipo Nullable<T> genérico , onde Té o tipo a ser levantado. As conversões são definidas implicitamente para converter entre os valores da base e do tipo elevado. O tipo levantado pode ser comparado nullou testado HasValue. Além disso, os operadores levantados são implícita e automaticamente definidos com base em sua base não levantada, onde - com exceção de alguns operadores booleanos - um argumento nulo se propagará para o resultado.

Java não suporta levantamento de tipo como um conceito, mas todos os tipos primitivos integrados têm tipos de wrapper correspondentes, que suportam o nullvalor em virtude de serem tipos de referência (classes).

De acordo com a especificação Java, qualquer tentativa de cancelar a nullreferência da referência deve resultar em uma exceção sendo lançada em tempo de execução, especificamente a NullPointerException. (Não faria sentido desreferenciá-lo de outra forma, porque, por definição, ele não aponta para nenhum objeto na memória.) Isso também se aplica ao tentar desempacotar uma variável de um tipo de invólucro, que avalia null: o programa lançará uma exceção, porque não há nenhum objeto a ser desempacotado - e, portanto, nenhum valor in a box para tomar parte no cálculo subsequente.

O exemplo a seguir ilustra o comportamento diferente. Em C #, o operador levantado * propaga o nullvalor do operando; em Java, desempacotar a referência nula lança uma exceção.

Nem todos os operadores levantados C # foram definidos para se propagar nullincondicionalmente, se um dos operandos for null. Especificamente, os operadores booleanos foram elevados para suportar a lógica ternária , mantendo assim a impedância com o SQL .

Os operadores booleanos Java não oferecem suporte à lógica ternária, nem são implementados na biblioteca de classes base.

Tipo de ligação tardia (dinâmico)

C # apresenta um tipo dinâmico de ligação tardia que suporta invocação dinâmica sem reflexão, interoperabilidade com linguagens dinâmicas e ligação ad-hoc a (por exemplo) modelos de objeto de documento. O dynamictipo resolve o acesso do membro dinamicamente em tempo de execução, ao contrário de estaticamente / virtual em tempo de compilação. O mecanismo de pesquisa de membro é extensível com reflexão tradicional como um mecanismo de fallback.

Existem vários casos de uso para o dynamictipo em C #:

  • Uso menos detalhado de reflexão: ao lançar uma instância para o dynamictipo, membros como propriedades, métodos, eventos etc. podem ser chamados diretamente na instância sem usar a API de reflexão diretamente.
  • Interoperabilidade com linguagens dinâmicas: O tipo dinâmico vem com um suporte hub-and-spoke para a implementação de objetos digitados dinamicamente e infraestrutura de tempo de execução comum para pesquisa de membro eficiente.
  • Criação de abstrações dinâmicas em tempo real: por exemplo, um objeto dinâmico pode fornecer acesso mais simples a modelos de objetos de documentos, como documentos XML ou XHTML .

Java não oferece suporte a um tipo de ligação tardia. Os casos de uso para o tipo dinâmico C # têm diferentes construções correspondentes em Java:

  • Para invocação dinâmica de limite tardio por nome de tipos preexistentes, a reflexão deve ser usada.
  • Para interoperabilidade com linguagens dinâmicas, alguma forma de API de interoperabilidade específica para aquela linguagem deve ser usada. A plataforma da máquina virtual Java possui várias linguagens dinâmicas implementadas nela, mas não há um padrão comum sobre como passar objetos entre as linguagens. Normalmente, isso envolve alguma forma de reflexão ou API semelhante a reflexão. Como um exemplo de como usar objetos JavaFX de Java.
  • Para criar e interagir com objetos inteiramente em tempo de execução, por exemplo, interação com uma abstração de modelo de objeto de documento, uma API de abstração específica deve ser usada.

Veja também o exemplo #Interoperabilidade com linguagens dinâmicas .

Ponteiros

Java exclui ponteiros e aritmética de ponteiros no Java Runtime Environment. Os designers da linguagem Java raciocinaram que os ponteiros são um dos principais recursos que permitem aos programadores colocar bugs em seu código e optaram por não suportá-los. Java não permite a passagem e recepção direta de objetos / estruturas de / para o sistema operacional subjacente e, portanto, não precisa modelar objetos / estruturas para um layout de memória específico, layouts que frequentemente envolvem ponteiros. A comunicação do Java com o sistema operacional subjacente é, em vez disso, baseada na Java Native Interface (JNI), onde a comunicação com / a adaptação a um sistema operacional subjacente é tratada por meio de uma camada de cola externa.

Embora o C # permita o uso de ponteiros e aritmética de ponteiros correspondentes, os designers da linguagem C # tinham a mesma preocupação de que ponteiros poderiam ser usados ​​para contornar as regras estritas de acesso a objetos. Portanto, C # por padrão também exclui ponteiros. No entanto, como os ponteiros são necessários ao chamar muitas funções nativas, eles são permitidos em um modo não seguro explícito . Os blocos de código ou métodos que usam os ponteiros devem ser marcados com a unsafepalavra-chave para poder usar os ponteiros, e o compilador requer a /unsafeopção para permitir a compilação desse código. Os assemblies compilados usando a /unsafeopção são marcados como tal e só podem ser executados se forem explicitamente confiáveis. Isso permite o uso de ponteiros e aritmética de ponteiros para passar e receber objetos diretamente de / para o sistema operacional ou outras APIs nativas usando o layout de memória nativa para esses objetos enquanto também isola esse código potencialmente inseguro em assemblies especificamente confiáveis.

Tipos de referência

Em ambas as línguas, as referências são um conceito central. Todas as instâncias de classes são por referência .

Embora não seja diretamente evidente na sintaxe da linguagem em si , ambas as linguagens suportam o conceito de referências fracas . Uma instância que é referenciada apenas por referências fracas é elegível para a coleta de lixo como se não houvesse nenhuma referência. Em ambas as linguagens, esse recurso é exposto por meio das bibliotecas associadas, embora seja realmente um recurso de tempo de execução central.

Junto com referências fracas, Java tem referências suaves . Eles são muito parecidos com referências fracas, mas a JVM não desalocará objetos referenciados suavemente até que a memória seja necessária.

Tipos de referência Java C #
Coleta de lixo sim sim
Referências fracas sim sim
Fila de referência (interação com a coleta de lixo) sim sim
Referências suaves sim Não
Referências fantasmas sim Não
Suporte de proxy Sim; geração de proxy Sim; contextos de objeto

Matrizes e coleções

Matrizes e coleções são conceitos apresentados por ambas as linguagens.

Matrizes e coleções Java C #
Tipos de dados abstratos sim sim
Matrizes de índice unidimensionais baseadas em zero sim sim
Matrizes multidimensionais, retangulares (matriz única) Não sim
Matrizes multidimensionais, denteadas (matrizes de matrizes) sim sim
Matrizes não baseadas em zero Não Algum
Matrizes e coleções unificadas Não sim
Mapas / dicionários sim sim
Dicionários classificados sim sim
Jogos sim sim
Conjuntos ordenados sim sim
Listas / vetores sim sim
Filas / pilhas sim sim
Fila de prioridade sim sim
Bolsas / multisets Biblioteca de terceiros sim
Coletas otimizadas para simultaneidade sim sim

A sintaxe usada para declarar e acessar arrays é idêntica, exceto que C # adicionou sintaxe para declarar e manipular arrays multidimensionais.

Java C #
Arrays são especializações implicitamente diretas de Object. Eles não são unificados com os tipos de coleção. Arrays em C # são especializações implícitas da System.Arrayclasse que implementa várias interfaces de coleção .
Arrays e coleções são completamente separados, sem unificação. Arrays não podem ser passados ​​onde sequências ou coleções são esperadas (embora eles possam ser agrupados usando Arrays.asList). Matrizes podem ser passadas onde sequências ( IEnumerables) ou coleções / interfaces de lista são esperadas. No entanto, as operações de coleção que alteram o número de elementos (inserir / adicionar / remover) lançarão exceções, pois essas operações não são suportadas por matrizes.
A forinstrução aceita matrizes ou Iterables. Todas as coleções são implementadas Iterable. Isso significa que a mesma sintaxe curta pode ser usada em loops for. A foreachinstrução itera por meio de uma sequência usando uma implementação específica do GetEnumeratormétodo, geralmente implementada por meio da interfaceIEnumerable ou . Como os arrays sempre implementam implicitamente essas interfaces , o loop também itera por meio de arrays. IEnumerable<T>
Em ambas as linguagens, as matrizes de tipos de referência são covariantes. Isso significa que uma String[]matriz pode ser atribuída a variáveis ​​de Object[], assim como Stringuma especialização de (atribuível a) Object. Em ambas as linguagens, os arrays realizarão uma verificação de tipo ao inserir novos valores, porque a segurança de tipo estaria comprometida. Isso contrasta com a forma como as coleções genéricas foram implementadas em ambas as linguagens.
Sem matrizes multidimensionais ( matrizes retangulares), mas matrizes de referências a matrizes ( matrizes denteadas ). Matrizes multidimensionais ( matrizes retangulares) e matrizes de referências a matrizes ( matrizes denteadas ).
As matrizes não podem ser redimensionadas (embora o uso do System.arraycopy()método possa permitir o redimensionamento de matrizes em várias etapas) Os arrays podem ser redimensionados enquanto preservam os valores existentes usando o Array.Resize()método de array estático (mas isso pode retornar um novo array).
Implementado como um retrofit para a java.utilbiblioteca com recursos extras, como estruturas de dados como conjuntos e conjuntos vinculados, e tem vários algoritmos para manipular elementos de uma coleção, como encontrar o maior elemento com base em algum Comparator<T>objeto, encontrar o menor elemento, encontrar sublistas dentro de um list, reverter o conteúdo de uma lista, embaralhar o conteúdo de uma lista, criar versões imutáveis ​​de uma coleção, realizar classificações e fazer pesquisas binárias. O quadro C # coleções consiste em aulas do System.Collectionse os System.Collections.Genericnamespaces com vários úteis de interfaces , classes abstratas, e estruturas de dados. NET 3.5 adicionado System.Linqespaço de nomes que contém vários métodos de extensão para colecções consultando, tais como Aggregate, All, Average, Distinct, Join, Unione muitos outros. As consultas que usam esses métodos são chamadas de Consulta Integrada à Linguagem (LINQ).

Os arrays multidimensionais podem, em alguns casos, aumentar o desempenho devido ao aumento da localidade (já que há uma desreferência de ponteiro em vez de um para cada dimensão do array, como é o caso dos arrays irregulares). No entanto, uma vez que todo o acesso ao elemento da matriz em uma matriz multidimensional requer multiplicação / deslocamento entre as duas ou mais dimensões, isso é uma vantagem apenas em cenários de acesso muito aleatórios.

Outra diferença é que todo o array multidimensional pode ser alocado com um único aplicativo de operador new, enquanto os arrays irregulares requerem loops e alocações para cada dimensão. No entanto, o Java fornece uma construção sintática para alocar uma matriz irregular com comprimentos regulares; os loops e múltiplas alocações são então executados pela máquina virtual e não precisam ser explícitos no nível de origem.

Ambos os idiomas apresentam um amplo conjunto de tipos de coleção que inclui vários tipos ordenados e não ordenados de listas, mapas / dicionários, conjuntos, etc.

Java também suporta a sintaxe de C / C ++:

Expressões e operadores

Expressões e operadores Java C #
Operadores aritméticos sim sim
Operadores lógicos sim sim
Operadores lógicos bit a bit sim sim
Condicional sim sim
Concatenação de string sim sim
Casts sim sim
Boxe Sim; implícito Sim; implícito
Unboxing Sim; implícito Sim; explícito
Operadores levantados Não, mas veja java.util.Optional sim
Controle de estouro Não sim
Avaliação estrita de ponto flutuante Sim; opt-in / out Sim; opt-in
Strings ao pé da letra (aqui-) sim sim

Boxe e unboxing

Ambas as linguagens permitem boxing e unboxing automáticos , ou seja, permitem a conversão implícita entre quaisquer tipos primitivos e os tipos de referência correspondentes.
Em C #, os tipos primitivos são subtipos do tipo Object. Em Java, isso não é verdade; qualquer tipo primitivo fornecido e o tipo de wrapper correspondente não têm relacionamento específico entre si, exceto para autoboxing e unboxing, que atuam como açúcar sintático para a troca entre eles. Isso foi feito intencionalmente, para manter a compatibilidade com versões anteriores do Java, nas quais nenhuma conversão automática era permitida, e o programador trabalhava com dois conjuntos separados de tipos: os tipos primitivos e a hierarquia de tipos de wrapper (referência).

Essa diferença tem as seguintes consequências. Em primeiro lugar, em C #, os tipos primitivos podem definir métodos, como uma substituição do ToString()método de Object . Em Java, essa tarefa é realizada pelas classes de wrapper primitivas .
Em segundo lugar, em Java, um elenco extra é necessário sempre que alguém tenta desreferenciar diretamente um valor primitivo, uma vez que ele não será encaixotado automaticamente. A expressão ((Integer)42).toString()converterá um literal inteiro em string em Java enquanto 42.ToString()executa a mesma operação em C #. Isso ocorre porque o último é uma chamada de instância no valor primitivo 42, enquanto o primeiro é uma chamada de instância em um objeto do tipo java.lang.Integer.

Finalmente, outra diferença é que o Java faz uso intenso de tipos em caixa nos genéricos (veja abaixo ).

Afirmações

Afirmações Java C #
rotações sim sim
Condicionais sim sim
Controle de fluxo sim sim
Atribuição sim sim
Controle de exceção sim sim
Declaração de variável sim sim
Inferência de tipo de variável sim sim
Eliminação determinística (blocos ARM) sim sim

Sintaxe

Ambas as linguagens são consideradas linguagens de "chave" na família C / C ++. No geral, as sintaxes das linguagens são muito semelhantes. A sintaxe no nível de instrução e expressão é quase idêntica à inspiração óbvia da tradição C / C ++. No nível de definição de tipo (classes e interfaces ) existem algumas pequenas diferenças. Java é explícito sobre como estender classes e implementar interfaces , enquanto C # infere isso a partir dos tipos de tipos dos quais uma nova classe / interface deriva.

C # oferece suporte a mais recursos do que Java, o que até certo ponto também é evidente na sintaxe que especifica mais palavras-chave e mais regras gramaticais do que Java.

Palavras-chave e compatibilidade com versões anteriores

Conforme as linguagens evoluíram, os designers de linguagens para ambas as linguagens enfrentaram situações em que queriam estender as linguagens com novas palavras-chave ou sintaxe. Novas palavras-chave em particular podem quebrar o código existente no nível de origem, ou seja, o código mais antigo pode não ser mais compilado, se apresentado a um compilador para uma versão posterior da linguagem. Os designers de linguagem estão ansiosos para evitar tais regressões. Os projetistas das duas linguagens têm seguido caminhos diferentes ao abordar esse problema.

Os designers da linguagem Java evitaram novas palavras-chave tanto quanto possível, preferindo, em vez disso, introduzir novas construções sintáticas que antes não eram legais ou reutilizar palavras-chave existentes em novos contextos. Dessa forma, eles não prejudicaram a compatibilidade com versões anteriores. Um exemplo do primeiro pode ser encontrado em como o forloop foi estendido para aceitar tipos iteráveis. Um exemplo do último pode ser encontrado em como as palavras extends- superchave e (especialmente) as palavras - chave foram reutilizadas para especificar limites de tipo quando os genéricos foram introduzidos no Java 1.5. Em certa época (Java 1.4), assertfoi introduzida uma nova palavra- chave que antes não estava reservada como palavra-chave. Isso tinha o potencial de tornar inválido código anteriormente válido, se, por exemplo, o código usado assertcomo um identificador. Os designers optaram por resolver este problema com uma solução de quatro etapas: 1) Apresentando uma opção de compilador que indica se Java 1.4 ou posterior deve ser usado, 2) Marcando apenas assertcomo uma palavra-chave ao compilar como Java 1.4 e posterior, 3) Padronizando para 1.3 para evitar tornar inválido o código anterior (código não compatível com 1.4) e 4) Emitir avisos, se a palavra-chave for usada no modo Java 1.3, para permitir que os desenvolvedores alterem o código.

Os designers da linguagem C # introduziram várias palavras-chave novas desde a primeira versão. No entanto, em vez de definir essas palavras-chave como palavras-chave globais , eles as definem como palavras-chave sensíveis ao contexto . Isso significa que mesmo quando eles introduziram (entre outras) as palavras partial- yieldchave e no C # 2.0, o uso dessas palavras como identificadores ainda é válido, pois não há conflito possível entre o uso como palavra-chave e o uso como identificador, dado o contexto. Portanto, a sintaxe C # atual é totalmente compatível com versões anteriores do código-fonte escrito para qualquer versão anterior, sem especificar a versão do idioma a ser usado.

palavra-chave recurso, exemplo de uso
checked, unchecked Em C #, os checkedblocos de instrução ou expressões podem habilitar a verificação em tempo de execução para estouro aritmético .
get, set C # implementa propriedades como parte da sintaxe da linguagem com seus correspondentes gete setacessores opcionais , como uma alternativa para os métodos do acessador usados ​​em Java, que não é um recurso de linguagem, mas um padrão de codificação baseado em convenções de nome de método.
goto C # suporta a gotopalavra - chave. Isso pode ser útil ocasionalmente, por exemplo, para implementar máquinas de estado finito ou para código gerado , mas o uso de um método mais estruturado de fluxo de controle é geralmente recomendado (consulte as críticas à instrução goto ). Java não suporta a gotoinstrução (mas gotoé uma palavra reservada). No entanto, o Java oferece suporte às instruções rotuladas breake continue, que em certas situações podem ser usadas quando uma gotoinstrução poderia ser usada de outra forma.
switch(color)
{
    case Color.Blue:
        Console.WriteLine("Color is blue");
        break;
    case Color.DarkBlue:
        Console.WriteLine("Color is dark");
        goto case Color.Blue;
    // ...
}
lock Em C #, a lockpalavra-chave é uma abreviação para sincronizar o acesso a um bloco de código entre threads (usando a Monitor), embrulhado em um try... finallybloco.
out, ref C # tem suporte para parâmetros de saída e referência . Isso permite retornar vários valores de saída de um método ou passar valores por referência.
strictfp O Java usa strictfppara garantir que os resultados das operações de ponto flutuante permaneçam os mesmos em todas as plataformas.
switch Em C #, a instrução switch também opera em strings e longs. O fallthrough é permitido para instruções vazias e possível por meio de 'goto case' para instruções contendo código. A instrução switch do Java opera em strings (desde Java 7 ), mas não no longtipo primitivo, e falha em todas as instruções (excluindo aquelas com ' break').
synchronized Em Java, a synchronizedpalavra-chave é uma abreviatura para sincronizar o acesso a um bloco de código entre threads (usando um Monitor), embrulhado em um try... finallybloco.
throws Java requer que cada método declare as exceções verificadas ou superclasses das exceções verificadas que ele pode lançar. Qualquer método também pode declarar opcionalmente a exceção não verificada que ele lança. C # não tem essa sintaxe.
public int readItem() throws java.io.IOException {
    // ...
}
using Em C #, usingfaz com que o Disposemétodo (implementado por meio da IDisposable interface ) do objeto declarado seja executado após a execução do bloco de código ou quando uma exceção é lançada dentro do bloco de código.
// Create a small file "test.txt", write a string,
// ... and close it (even if an exception occurs)
using (StreamWriter file = new StreamWriter("test.txt"))
{
    file.Write("test");
}

Em Java SE 7, uma construção semelhante foi adicionada chamada try-with-resources:

try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

Programação orientada a objetos

Tanto C # quanto Java são projetados desde o início como linguagens orientadas a objetos usando despacho dinâmico , com sintaxe semelhante a C ++ (C ++, por sua vez, deriva de C ). Nenhuma das linguagens é um superconjunto de C ou C ++, entretanto.

Orientação do objeto Java C #
Aulas obrigatório obrigatório
Interfaces sim sim
Classes abstratas sim sim
Níveis de acessibilidade do membro Sim; público, pacote, protegido, privado Sim; público, interno, protegido, privado, interno protegido
Classes internas de nível de classe Sim; staticclasses internas são nível de classe Sim; todas as classes internas são de nível de classe
Classes internas em nível de instância sim Não
Classes anônimas em nível de instrução (local) sim Sim; mas sem métodos
Aulas parciais Não; Biblioteca de terceiros sim
Classes anônimas implícitas (inferidas) Não sim
Suspensão de uso / obsolescência sim sim
Sobrecarga de controle de versão Algum sim
Enums podem implementar interfaces sim Não
Propriedades Não, mas veja as especificações JavaBeans sim
Eventos Fornecido por bibliotecas padrão Recurso de linguagem integrado
Sobrecarga do operador Não sim
Indexadores Não sim
Conversões implícitas Não; mas veja autoboxing sim
Conversões explícitas sim sim

Aula parcial

C # permite que uma definição de classe seja dividida em vários arquivos de origem usando um recurso chamado classes parciais . Cada parte deve ser marcada com a palavra-chave partial. Todas as partes devem ser apresentadas ao compilador como parte de uma única compilação. As peças podem fazer referência a membros de outras peças. As partes podem implementar interfaces e uma parte pode definir uma classe base. O recurso é útil em cenários de geração de código (como design de interface de usuário (IU)), onde um gerador de código pode fornecer uma parte e o desenvolvedor outra parte para serem compilados juntos. O desenvolvedor pode, portanto, editar sua parte sem o risco de um gerador de código sobrescrever esse código posteriormente. Ao contrário do mecanismo de extensão de classe, uma classe parcial permite dependências circulares entre suas partes, uma vez que têm a garantia de serem resolvidas em tempo de compilação. Java não tem um conceito correspondente.

Classes internas e locais

Ambas as linguagens permitem classes internas , onde uma classe é definida lexicamente dentro de outra classe. No entanto, em cada idioma, essas classes internas têm semânticas bastante diferentes.

Em Java, a menos que a classe interna seja declarada static, uma referência a uma instância de uma classe interna carrega uma referência à classe externa com ela. Como resultado, o código da classe interna tem acesso aos membros estáticos e não estáticos da classe externa. Para criar uma instância de uma classe interna não estática, a instância da classe externa abrangente deve ser nomeada. Isto é feito através de um novo new-Operador introduzido em JDK 1.3: outerClassInstance.new Outer.InnerClass(). Isso pode ser feito em qualquer classe que tenha uma referência a uma instância da classe externa.

Em C #, uma classe interna é conceitualmente igual a uma classe normal. Em certo sentido, a classe externa atua apenas como um namespace. Portanto, o código da classe interna não pode acessar membros não estáticos da classe externa, a menos que o faça por meio de uma referência explícita a uma instância da classe externa. Os programadores podem declarar a classe interna privada para permitir que apenas a classe externa tenha acesso a ela.

Java fornece outro recurso chamado classes locais ou classes anônimas , que podem ser definidas dentro de um corpo de método. Geralmente, eles são usados ​​para implementar uma interface com apenas um ou dois métodos, que normalmente são manipuladores de eventos. No entanto, eles também podem ser usados ​​para substituir métodos virtuais de uma superclasse. Os métodos nessas classes locais têm acesso às variáveis ​​locais do método externo declaradas final. C # satisfaz os casos de uso para esses, fornecendo delegados anônimos ; veja manipulação de eventos para mais informações sobre isso.

C # também fornece um recurso chamado tipos / classes anônimos , mas é bastante diferente do conceito de Java com o mesmo nome. Ele permite que o programador instancie uma classe fornecendo apenas um conjunto de nomes para as propriedades que a classe deve ter e uma expressão para inicializar cada uma. Os tipos das propriedades são inferidos dos tipos dessas expressões. Essas classes declaradas implicitamente são derivadas diretamente do objeto .

Evento

Os delegados multicast C # são usados ​​com eventos . Os eventos fornecem suporte para programação orientada a eventos e são uma implementação do padrão do observador . Para suportar isso, existe uma sintaxe específica para definir eventos em classes e operadores para registrar, cancelar o registro ou combinar manipuladores de eventos.

Veja aqui informações sobre como os eventos são implementados em Java.

Sobrecarga de operador e conversões

A sobrecarga do operador e as projeções definidas pelo usuário são recursos separados que visam permitir que novos tipos se tornem cidadãos de primeira classe no sistema de tipos. Ao usar esses recursos em C #, tipos como Complexe decimalforam integrados para que os operadores usuais, como adição e multiplicação, funcionem com os novos tipos. Ao contrário de C ++, C # faz restringir o uso de sobrecarga de operador, impedindo-o para os operadores new, ( ), ||, &&, =, e quaisquer variações de instruções compostas como +=. Mas os operadores compostos chamarão operadores simples sobrecarregados, como -=chamar -e =.

Java não inclui sobrecarga de operador, nem conversões personalizadas para evitar o abuso do recurso e manter a linguagem simples.

Indexador

C # também inclui indexadores que podem ser considerados um caso especial de sobrecarga de operador (como o C ++ operator[]) ou parametrizados get/ setpropriedades. Um indexador é uma propriedade nomeada this[]que usa um ou mais parâmetros (índices); os índices podem ser objetos de qualquer tipo:

myList[4] = 5;
string name = xmlNode.Attributes["name"];
orders = customerMap[theCustomer];

Java não inclui indexadores. O padrão Java comum envolve escrever getters e setters explícitos onde um programador C # usaria um indexador.

Campos e inicialização

Campos e inicialização Java C #
Campos sim sim
Constantes sim Sim; mas sem suporte para parâmetros constantes passados
Construtores estáticos (classe) sim sim
Construtores de instância sim sim
Finalizadores / destruidores sim sim
Inicializadores de instância sim Não; pode ser simulado com o construtor de instância
Inicialização de objeto De baixo para cima
(campos e construtores)
Top-down (campos); ascendente (construtores)
Inicializadores de objeto sim sim
Inicializadores de coleção Não; métodos varargs estáticos sim
Inicializadores de array sim sim

Inicialização de objeto

Em C # e Java, os campos de um objeto podem ser inicializados por inicializadores de variáveis (expressões que podem ser atribuídas a variáveis ​​onde são definidas) ou por construtores (sub-rotinas especiais que são executadas quando um objeto está sendo criado). Além disso, Java contém inicializadores de instância , que são blocos anônimos de código sem argumentos que são executados após a chamada explícita (ou implícita) para o construtor de uma superclasse, mas antes que o construtor seja executado.

C # inicializa campos de objeto na seguinte ordem ao criar um objeto:

  1. Campos estáticos derivados
  2. Construtor estático derivado
  3. Campos de instância derivados
  4. Campos estáticos de base
  5. Construtor estático de base
  6. Campos de instância base
  7. Construtor de instância base
  8. Construtor de instância derivada

Alguns dos campos acima podem não ser aplicáveis ​​(por exemplo, se um objeto não tiver campos estáticos ). Os campos derivados são aqueles definidos na classe direta do objeto, enquanto o campo base é um termo para os campos definidos em uma das superclasses do objeto. Observe que uma representação de objeto na memória contém todos os campos definidos em sua classe ou qualquer uma de suas superclasses, mesmo se alguns campos em superclasses forem definidos como privados.

É garantido que quaisquer inicializadores de campo entrem em vigor antes que qualquer construtor seja chamado, uma vez que o construtor de instância da classe do objeto e suas superclasses são chamados depois que os inicializadores de campo são chamados. Há, no entanto, uma armadilha potencial na inicialização do objeto quando um método virtual é chamado de um construtor de base. O método substituído em uma subclasse pode fazer referência a um campo definido na subclasse, mas esse campo pode não ter sido inicializado porque o construtor da subclasse que contém a inicialização do campo é chamado após o construtor de sua classe base.

Em Java, a ordem de inicialização é a seguinte:

  1. Invocação de outro construtor (seja da classe do objeto ou da superclasse do objeto)
  2. Inicializadores de variável de instância e inicializadores de instância (na ordem em que aparecem no código-fonte)
  3. O corpo do construtor

Como no C #, um novo objeto é criado chamando um construtor específico. Dentro de um construtor, a primeira instrução pode ser uma invocação de outro construtor. Se for omitido, a chamada ao construtor sem argumentos da superclasse é adicionada implicitamente pelo compilador. Caso contrário, outro construtor sobrecarregado da classe do objeto pode ser chamado explicitamente ou um construtor de superclasse pode ser chamado. No primeiro caso, o construtor chamado chamará novamente outro construtor (seja da classe do objeto ou de sua subclasse) e a cadeia, mais cedo ou mais tarde, termina na chamada de um dos construtores da superclasse.

Depois que outro construtor é chamado (que causa a invocação direta do construtor da superclasse e assim por diante, até a classe Object), as variáveis ​​de instância definidas na classe do objeto são inicializadas. Mesmo se não houver inicializadores de variáveis ​​explicitamente definidos para algumas variáveis, essas variáveis ​​são inicializadas com os valores padrão. Observe que as variáveis ​​de instância definidas nas superclasses já foram inicializadas neste ponto, porque foram inicializadas por um construtor da superclasse quando foi chamado (pelo código do construtor ou por inicializadores de variáveis ​​executados antes do código do construtor ou implicitamente para os valores padrão). Em Java, os inicializadores de variáveis ​​são executados de acordo com sua ordem textual no arquivo de origem.

Finalmente, o corpo do construtor é executado. Isso garante a ordem apropriada de inicialização, ou seja, os campos de uma classe base terminam a inicialização antes que a inicialização dos campos de uma classe de objeto comece.

Existem duas armadilhas potenciais principais na inicialização do objeto Java. Primeiro, os inicializadores de variáveis ​​são expressões que podem conter chamadas de método. Como os métodos podem fazer referência a qualquer variável definida na classe, o método chamado em um inicializador de variável pode fazer referência a uma variável que é definida abaixo da variável que está sendo inicializada. Visto que a ordem de inicialização corresponde à ordem textual de definições de variáveis, tal variável não seria inicializada com o valor prescrito por seu inicializador e conteria o valor padrão. Outra armadilha potencial é quando um método que é substituído na classe derivada é chamado no construtor da classe base, o que pode levar a um comportamento que o programador não esperaria quando um objeto da classe derivada fosse criado. De acordo com a ordem de inicialização, o corpo do construtor da classe base é executado antes que os inicializadores de variáveis ​​sejam avaliados e antes que o corpo do construtor da classe derivada seja executado. O método substituído chamado do construtor da classe base pode, no entanto, fazer referência a variáveis ​​definidas na classe derivada, mas ainda não foram inicializados com os valores especificados por seus inicializadores ou definidos no construtor da classe derivada. O último problema se aplica ao C # também, mas de uma forma menos crítica, pois no C # os métodos não podem ser substituídos por padrão.

Eliminação de recursos

Ambas as linguagens usam principalmente a coleta de lixo como meio de recuperar recursos de memória, em vez de desalocação explícita de memória. Em ambos os casos, se um objeto contém recursos de diferentes tipos além da memória, como identificadores de arquivo, recursos gráficos, etc., ele deve ser notificado explicitamente quando o aplicativo não o usa mais. Tanto o C # quanto o Java oferecem interfaces para esse descarte determinístico e tanto o C # quanto o Java (desde o Java 7) apresentam instruções de gerenciamento automático de recursos que invocarão automaticamente os métodos de descarte / fechamento nessas interfaces.

Métodos

Métodos e propriedades Java C #
Importações estáticas sim sim
Métodos virtuais Virtual por padrão Não virtual por padrão
Resumo sim sim
Selagem sim sim
Implementação de interface explícita Métodos padrão sim
Parâmetros de valor (entrada) sim sim
Parâmetros de referência (entrada / saída) Não sim
Parâmetros de saída (saída) Não sim
Parâmetros constantes (imutáveis) Sim; parâmetros finais sim
Métodos Variadic sim sim
Argumentos opcionais Não; Em vez disso, sobrecarga de método ou varargs sim
Argumentos nomeados Não sim
Métodos geradores Não sim
Métodos de extensão / padrão sim sim
Métodos condicionais Não sim
Métodos parciais Não sim

Métodos de extensão e métodos padrão

Usando um especial esta designação em primeiro parâmetro de um método, C # permite que o método para actuar como se fosse um método membro do tipo do primeiro parâmetro. Esta extensão da classe estrangeira é puramente sintática. O método de extensão deve ser declarado statice definido em uma classe puramente estática. O método deve obedecer a qualquer restrição de acesso de membro como qualquer outro método externo à classe; portanto, os métodos estáticos não podem quebrar o encapsulamento do objeto. A "extensão" está ativa apenas nos escopos em que o namespace da classe de host estática foi importado.

Desde o Java 8, o Java possui um recurso semelhante chamado métodos padrão , que são métodos com um corpo declarado nas interfaces. Ao contrário dos métodos de extensão C #, os métodos padrão Java são métodos de instância na interface que os declara. A definição de métodos padrão nas classes que implementam a interface é opcional: se a classe não definir o método, a definição padrão será usada.

Os métodos de extensão C # e os métodos padrão Java permitem que uma classe substitua a implementação padrão do método de extensão / padrão, respectivamente. Em ambas as linguagens, essa substituição é obtida definindo um método na classe que deve usar uma implementação alternativa do método.

As regras de escopo do C # definem que, se um método de correspondência for encontrado em uma classe, ele terá precedência sobre um método de extensão de correspondência. Em Java, qualquer classe declarada para implementar uma interface com o método padrão é assumida como tendo as implementações dos métodos padrão, a menos que a classe implemente o próprio método.

Métodos parciais

Relacionado a classes parciais, C # permite que métodos parciais sejam especificados em classes parciais. Um método parcial é uma declaração intencional de um método com várias restrições à assinatura. As restrições garantem que, se uma definição não for fornecida por nenhuma parte da classe, o método e todas as chamadas a ele poderão ser apagados com segurança. Esse recurso permite que o código forneça um grande número de pontos de interceptação (como o padrão de design do método de modelo GoF ) sem pagar qualquer sobrecarga de tempo de execução se esses pontos de extensão não estiverem sendo usados ​​por outra parte da classe em tempo de compilação. Java não tem um conceito correspondente.

Métodos virtuais

Os métodos em C # não são virtuais por padrão e devem ser declarados virtuais explicitamente, se desejado. Em Java, todos os métodos não privados não estáticos são virtuais. A virtualidade garante que a substituição mais recente para o método sempre será chamada, mas incorre em um certo custo de tempo de execução na chamada, pois essas chamadas não podem ser embutidas normalmente e requerem uma chamada indireta por meio da tabela de método virtual . No entanto, algumas implementações JVM, incluindo a implementação de referência Oracle, implementam inlining dos métodos virtuais mais comumente chamados.

Os métodos Java são virtuais por padrão (embora possam ser lacrados usando o finalmodificador para não permitir a substituição). Não há como permitir que as classes derivadas definam um método novo e não relacionado com o mesmo nome.

Isso significa que, por padrão em Java, e apenas quando explicitamente habilitado em C #, novos métodos podem ser definidos em uma classe derivada com o mesmo nome e assinatura que aqueles em sua classe base. Quando o método é chamado em uma referência de superclasse de tal objeto, a implementação "mais profunda" sobrescrita do método da classe base será chamada de acordo com a subclasse específica do objeto que está sendo referenciado.

Em alguns casos, quando uma subclasse introduz um método com o mesmo nome e assinatura de um método já presente na classe base , podem ocorrer problemas. Em Java, isso significa que o método na classe derivada substituirá implicitamente o método na classe base, mesmo que essa não seja a intenção dos designers de nenhuma das classes.

Para atenuar isso, o C # exige que, se um método se destina a substituir um método herdado, a overridepalavra - chave deve ser especificada. Caso contrário, o método "ocultará" o método herdado. Se a palavra-chave estiver ausente, é emitido um aviso do compilador para esse efeito, que pode ser silenciado especificando-se a newpalavra - chave. Isso evita o problema que pode surgir de uma classe base sendo estendida com um método não privado (ou seja, uma parte herdada do namespace) cuja assinatura já está em uso por uma classe derivada. Java tem uma verificação de compilador semelhante na forma de @Overrideanotação do método, mas não é obrigatória e, na sua ausência, a maioria dos compiladores não fornecerá comentários (mas o método será sobrescrito).

Parâmetros constantes / imutáveis

Em Java, é possível evitar a reatribuição de uma variável local ou parâmetro de método usando a finalpalavra - chave. Aplicar esta palavra - chave a uma variável de tipo primitivo faz com que a variável se torne imutável. No entanto, aplicar finala uma variável de tipo de referência apenas evita que outro objeto seja atribuído a ela. Isso não impedirá que os dados contidos no objeto sofram mutação. A partir do C # 7, é possível evitar a reatribuição de um parâmetro de método usando a inpalavra - chave; no entanto, essa palavra-chave não pode ser usada em variáveis ​​locais. Como com Java, aplicar ina um parâmetro apenas evita que o parâmetro seja reatribuído a um valor diferente. Ainda é possível alterar os dados contidos no objeto.

Java C #
public int addOne(final int x) {
    x++; // ERROR: a final variable cannot be reassigned
    return x;
}

public List addOne(final List<Integer> list) {
    list.add(1); // OK: it is still possible to modify a
                 // final (reference type) variable
    return list;
}
public int AddOne(in int x) 
{
    x++; // ERROR: a readonly parameter cannot be reassigned
    return x;
}

public List<int> AddOne(in List<int> list) 
{
    list.Add(1); // OK: it is still possible to modify a
                 // readonly (reference type) parameter
    return list;
}

Ambas as linguagens não oferecem suporte ao recurso essencial de correção constante que existe em C / C ++ , o que torna um método constante.

Java define a palavra "constante" arbitrariamente como um campo. Por convenção, esses nomes de variáveis ​​são apenas maiúsculos com palavras separadas por um sublinhado, mas a linguagem Java não insiste nisso. Um parâmetro que é apenas não é considerado uma constante, embora possa sê-lo no caso de um tipo de dado primitivo ou de uma classe imutável , como a . static finalfinalString

Métodos geradores

Qualquer C # método declarado como retornando IEnumerable, IEnumeratorou as versões genéricas de essas interfaces podem ser implementadas usando yielda sintaxe. Essa é uma forma de continuação limitada gerada pelo compilador e pode reduzir drasticamente o código necessário para percorrer ou gerar sequências, embora esse código seja gerado apenas pelo compilador. O recurso também pode ser usado para implementar sequências infinitas, por exemplo, a sequência de números de Fibonacci .

Java não possui um recurso equivalente. Em vez disso, os geradores são normalmente definidos pelo fornecimento de uma implementação especializada de uma coleção bem conhecida ou interface iterável, que computará cada elemento sob demanda. Para que tal gerador seja usado em um para cada instrução, ele deve implementar interface java.lang.Iterable.

Veja também o exemplo de sequência de Fibonacci abaixo.

Implementação de interface explícita

C # também tem implementação de interface explícita que permite a uma classe implementar métodos especificamente de uma interface , separar seus próprios métodos de classe ou fornecer implementações diferentes para dois métodos com o mesmo nome e assinatura herdada de duas interfaces básicas.

Em qualquer um dos idiomas, se um método (ou propriedade em C #) for especificado com o mesmo nome e assinatura em várias interfaces, os membros entrarão em conflito quando for projetada uma classe que implemente essas interfaces. Uma implementação irá, por padrão, implementar um método comum para todas as interfaces. Se implementações separadas forem necessárias (porque os métodos servem a propósitos separados ou porque os valores de retorno diferem entre as interfaces), a implementação explícita da interface do C # resolverá o problema, embora permitindo resultados diferentes para o mesmo método, dependendo do elenco atual do objeto. Em Java, não há maneira de resolver esse problema a não ser refatorar uma ou mais interfaces para evitar conflitos de nomes.

Parâmetros de referência (entrada / saída)

Os argumentos de tipos primitivos (por exemplo, int, double) para um método são passados ​​por valor em Java, enquanto os objetos são passados ​​por referência. Isso significa que um método opera em cópias das primitivas passadas a ele, em vez de nas variáveis ​​reais. Pelo contrário, os objetos reais em alguns casos podem ser alterados. No exemplo a seguir, o objeto String não é alterado. O objeto da classe 'a' é alterado.

Em C #, é possível impor uma referência com a refpalavra - chave, semelhante a C ++ e de certa forma a C. Este recurso de C # é particularmente útil quando se deseja criar um método que retorna mais de um objeto. Em Java, tentar retornar vários valores de um método não é suportado, a menos que um wrapper seja usado, neste caso denominado "Ref".

Java C #
class PassByRefTest {

    static class Ref<T> {
        T val;
        Ref(T val) { this.val = val; }
    }
    
    static void changeMe(Ref<String> s) {
        s.val = "Changed";
    }

    static void swap(Ref<Integer> x, Ref<Integer> y) {
        int temp = x.val;

        x.val = y.val;
        y.val = temp;
    }

    public static void main(String[] args) {
        var a = new Ref(5);
        var b = new Ref(10);
        var s = new Ref("still unchanged");
        
        swap(a, b);
        changeMe(s);

        System.out.println( "a = " + a.val + ", " +
                            "b = " + b.val + ", " +
                            "s = " + s.val );
    }
}
class PassByRefTest 
{

    public static void ChangeMe(out string s) 
    {
        s = "Changed";
    }

    public static void Swap(ref int x, ref int y) 
    {
        int temp = x;

        x = y;
        y = temp;
    }

    public static void Main(string[] args) 
    {
        int a = 5;
        int b = 10;
        string s = "still unchanged";

        Swap(ref a, ref b);
        ChangeMe(out s);

        System.Console.WriteLine("a = " + a + ", " +
                                 "b = " + b + ", " +
                                 "s = " + s);
    }
}
a = 10, b = 5, s = Changed a = 10, b = 5, s = Changed

Exceções

Exceções Java C #
Exceções verificadas sim Não
Tente pegar finalmente sim sim

Exceções verificadas

Java oferece suporte a exceções verificadas (junto com exceções não verificadas). C # oferece suporte apenas a exceções não verificadas. As exceções verificadas forçam o programador a declarar a exceção lançada em um método ou capturar a exceção lançada usando uma try-catchcláusula.

As exceções verificadas podem encorajar boas práticas de programação, garantindo que todos os erros sejam tratados. No entanto, Anders Hejlsberg , arquiteto-chefe da linguagem C #, argumenta que eles foram, em certa medida, um experimento em Java e que não mostraram valer a pena, exceto em pequenos programas de exemplo.

Uma crítica é que as exceções verificadas encorajam os programadores a usar um bloco catch vazio ( catch (Exception e) {}), que silenciosamente engole as exceções, em vez de permitir que as exceções se propaguem para uma rotina de tratamento de exceções de nível superior. Em alguns casos, no entanto, o encadeamento de exceções pode ser aplicado em vez disso, lançando novamente a exceção em uma exceção de wrapper. Por exemplo, se um objeto for alterado para acessar um banco de dados em vez de um arquivo, um SQLExceptionpode ser capturado e lançado novamente como um IOException, uma vez que o chamador pode não precisar saber o funcionamento interno do objeto.

No entanto, nem todos os programadores concordam com essa postura. James Gosling e outros afirmam que as exceções verificadas são úteis, e o uso indevido delas causou os problemas. Capturar exceções silenciosamente é possível, sim, mas deve ser declarado explicitamente o que fazer com a exceção, versus exceções não verificadas que permitem não fazer nada por padrão. Ele pode ser ignorado, mas o código deve ser escrito explicitamente para ignorá-lo.

Tente pegar finalmente

Também existem diferenças entre as duas línguas no tratamento da try-finallydeclaração. O finallybloco é sempre executado, mesmo se o trybloco contiver instruções de passagem de controle como throwou return. Em Java, isso pode resultar em um comportamento inesperado, se o trybloco for deixado por uma returninstrução com algum valor e, em seguida, o finallybloco que é executado posteriormente também for deixado por uma returninstrução com um valor diferente. C # resolve esse problema proibindo qualquer instrução de passagem de controle como returnou breakno finallybloco.

Uma razão comum para o uso de try-finallyblocos é proteger o código de gerenciamento de recursos, garantindo assim a liberação de recursos preciosos no bloco finally. C # apresenta a usinginstrução como uma abreviatura sintática para esse cenário comum, em que o Dispose()método do objeto de usingé sempre chamado.

Uma diferença bastante sutil é o momento em que um rastreamento de pilha é criado quando uma exceção está sendo lançada. Em Java, o rastreamento de pilha é criado no momento em que a exceção é criada.

class Foo {
    Exception up = new Exception();
    int foo() throws Exception {
        throw up;
    }
}

A exceção na instrução acima sempre conterá o rastreamento de pilha do construtor - não importa a freqüência com que foo seja chamado. Por outro lado, em C #, o rastreamento de pilha é criado no momento em que "throw" é executado.

class Foo
{
    Exception e = new Exception();
    int foo()
    {
        try
        {
            throw e;
        }
        catch (Exception e)
        {
            throw;
        }
    }
}

No código acima, a exceção conterá o rastreamento de pilha da primeira linha de lançamento. Ao capturar uma exceção, há duas opções caso a exceção deva ser relançada: throwirá apenas relançar a exceção original com a pilha original, enquanto criará throw eum novo rastreamento de pilha.

Finalmente bloqueia

Java permite que o fluxo de controle saia do finallybloco de uma tryinstrução, independentemente da forma como foi inserido. Isso pode fazer com que outra instrução de fluxo de controle (como return) seja encerrada no meio da execução. Por exemplo:

int foo() {
    try {
        return 0;
    } finally {
        return 1;
    }
}

No código acima, a returninstrução dentro do trybloco faz com que o controle saia dele e, portanto, o finallybloco é executado antes que o retorno real aconteça. No entanto, o finallypróprio bloco também executa um retorno. Assim, o retorno original que causou sua entrada não é executado, e o método acima retorna 1 em vez de 0. Informalmente, ele tenta retornar 0, mas finalmente retorna 1.

C # não permite nenhuma instrução que permita que o fluxo de controle saia do finallybloco prematuramente, exceto throw. Em particular, returnnão é permitido de forma alguma, gotonão é permitido se o rótulo de destino estiver fora do finallybloco continuee breaknão é permitido se o loop envolvente mais próximo estiver fora do finallybloco.

Genéricos

No campo dos genéricos, as duas linguagens mostram uma semelhança sintática superficial, mas têm profundas diferenças subjacentes.

Genéricos Java C #
Implementação Apagamento de tipo Reificação
Realização de tempo de execução Não sim
Variância de tipo Site de uso Site de declaração (apenas em interfaces)
Restrição de tipo de referência Sim; implícito sim
Restrição de valor / tipo primitivo Não sim
Restrição de construtor Não Sim (apenas para construtor sem parâmetros)
Restrição de subtipo sim sim
Restrição de supertipo sim Não
Compatibilidade de migração sim Não

Tipo erasure versus genéricos reificados

Genéricos em Java são uma construção apenas de linguagem; eles são implementados apenas no compilador. Os classfiles gerados incluem assinaturas genéricas apenas na forma de metadados (permitindo que o compilador compile novas classes neles). O tempo de execução não tem conhecimento do sistema de tipo genérico; os genéricos não fazem parte da JVM . Em vez disso, as classes e métodos genéricos são transformados durante a compilação por meio de um processo denominado eliminação de tipo . Durante isso, o compilador substitui todos os tipos genéricos por sua versão bruta e insere casts / checks apropriadamente no código do cliente onde o tipo e seus métodos são usados. O código de byte resultante não conterá referências a nenhum tipo ou parâmetro genérico (consulte também Genéricos em Java ).

A especificação da linguagem Java proíbe intencionalmente certos usos de genéricos; isso é necessário para permitir a implementação de genéricos por meio de eliminação de tipo e para permitir a compatibilidade de migração. A pesquisa para adicionar genéricos reificados à plataforma Java está em andamento, como parte do Projeto Valhalla .

C # se baseia no suporte a genéricos do sistema de execução virtual, ou seja, não é apenas um recurso de linguagem. A linguagem é meramente um front-end para suporte a genéricos entre linguagens no CLR . Durante a compilação, os genéricos são verificados quanto à exatidão, mas a geração de código para implementar os genéricos é adiada para o tempo de carregamento da classe. O código do cliente (código que invoca métodos / propriedades genéricos) é totalmente compilado e pode assumir que os genéricos são seguros para o tipo. Isso é chamado de reificação . Em tempo de execução, quando um conjunto único de parâmetros de tipo para uma classe / método / delegado genérico é encontrado pela primeira vez, o carregador / verificador de classe sintetiza um descritor de classe concreto e gera implementações de método. Durante a geração de implementações de método, todos os tipos de referência serão considerados um tipo, pois os tipos de referência podem compartilhar com segurança as mesmas implementações. Isso é apenas para fins de implementação de código. Diferentes conjuntos de tipos de referência ainda terão descritores de tipo exclusivos; suas tabelas de métodos simplesmente apontarão para o mesmo código.

A lista a seguir ilustra algumas diferenças entre Java e C # ao gerenciar genéricos. Não é exaustivo:

Java C #
Verificações de tipo e downcasts são injetados no código do cliente (o código que faz referência aos genéricos). Comparado ao código não genérico com conversões manuais, essas conversões serão iguais, mas comparadas ao código verificado em tempo de compilação que não precisaria de conversões e verificações em tempo de execução, essas operações representam uma sobrecarga de desempenho. Os genéricos C # / .net garantem a segurança de tipo e são verificados em tempo de compilação, tornando desnecessárias verificações / conversões extras em tempo de execução. Conseqüentemente, o código genérico será executado mais rápido do que o código não genérico (ou com apagamento do tipo) que requer conversões ao manipular objetos não genéricos ou com apagamento do tipo.
Não é possível usar tipos primitivos como parâmetros de tipo; em vez disso, o desenvolvedor deve usar o tipo de invólucro correspondente ao tipo primitivo. Isso incorre em sobrecarga de desempenho extra ao exigir conversões de boxing e unboxing, bem como uma pressão de coleta de memória e de lixo, já que os invólucros serão alocados no heap em vez de alocados na pilha. Tipos primitivos e de valor são permitidos como parâmetros de tipo em realizações genéricas. No tempo de execução, o código será sintetizado e compilado para cada combinação única de parâmetros de tipo no primeiro uso. Os genéricos que são realizados com tipo primitivo / valor não requerem conversões boxing / unboxing.
Exceções genéricas não são permitidas e um parâmetro de tipo não pode ser usado em uma cláusula catch Podem definir exceções genéricas e usá-las em cláusulas catch
Membros estáticos são compartilhados entre todas as realizações genéricas (durante a eliminação de tipo todas as realizações são dobradas em uma única classe) Os membros estáticos são separados para cada realização genérica. Uma realização genérica é uma classe única.
Os parâmetros de tipo não podem ser usados ​​em declarações de campos / métodos estáticos ou em definições de classes internas estáticas Sem restrições no uso de parâmetros de tipo
Não é possível criar uma matriz onde o tipo de componente é uma realização genérica (tipo parametrizado concreto)
Pair<String, String>[] tenPairs = new Pair[10]; //OK
Uma realização genérica é um cidadão de 1ª classe e pode ser usada como qualquer outra classe; também um componente de array
object tenPairs = new Pair<int, string>[10]; // OK
Não é possível criar uma matriz onde o tipo de componente é um parâmetro de tipo, mas é válido criar uma Objectmatriz e executar um typecast na nova matriz para obter o mesmo efeito.
public class Lookup<K, V> {
    public V[] getEmptyValues(K key) {
        return (V[]) new Object[0]; // OK
    }
}

Quando um parâmetro de tipo genérico está sob restrições de herança, o tipo de restrição pode ser usado em vez de Object

public class Lookup<K, V extends Comparable<V>> {
    public V[] getEmptyValues(K key) {
        return (V[]) new Comparable[0];
    }
}
Os parâmetros de tipo representam classes reais e discretas e podem ser usados ​​como qualquer outro tipo na definição genérica.
public class Lookup<K, V> {
    public V[] GetEmptyValues(K key) {
        return new V[0]; // OK
    }
}
Não há literal de classe para uma realização concreta de um tipo genérico Uma realização genérica é uma aula real.
instanceof não é permitido com parâmetros de tipo ou realizações genéricas concretas Os operadores ise asfuncionam da mesma forma para parâmetros de tipo e para qualquer outro tipo.
Não é possível criar novas instâncias usando um parâmetro de tipo como o tipo Com uma restrição de construtor, métodos genéricos ou métodos de classes genéricas podem criar instâncias de classes que possuem construtores padrão.
As informações de tipo são apagadas durante a compilação. Extensões especiais para reflexão devem ser usadas para descobrir o tipo original. As informações de tipo sobre os tipos genéricos do C # são totalmente preservadas em tempo de execução e permitem o suporte total à reflexão e a instanciação de tipos genéricos.
A reflexão não pode ser usada para construir novas realizações genéricas. Durante a compilação, códigos extras (typecasts) são injetados no código cliente dos genéricos. Isso impede a criação de novas realizações posteriormente. A reflexão pode ser usada para criar novas realizações para novas combinações de parâmetros de tipo.

C # permite genéricos diretamente para tipos primitivos. Java, em vez disso, permite o uso de tipos em caixas como parâmetros de tipo (por exemplo, em List<Integer>vez de List<int>). Isso tem um custo, pois todos esses valores precisam ser encaixotados / desencaixotados quando usados, e todos eles precisam ser alocados em heap. No entanto, um tipo genérico pode ser especializado com um tipo de array de um tipo primitivo em Java, por exemplo, List<int[]>é permitido. Várias bibliotecas de terceiros implementaram as coleções básicas em Java com matrizes primitivas de apoio para preservar o tempo de execução e a otimização de memória que os tipos primitivos fornecem.

Compatibilidade de migração

O design de eliminação de tipo do Java foi motivado por um requisito de design para obter compatibilidade de migração - não deve ser confundido com compatibilidade com versões anteriores . Em particular, o requisito original era " ... deve haver um caminho de migração limpo e demonstrável para as APIs de coleções que foram introduzidas na plataforma Java 2 ". Isso foi projetado de forma que quaisquer novas coleções genéricas devam ser passíveis de métodos que esperavam uma das classes de coleção pré-existentes.

Os genéricos C # foram introduzidos na linguagem enquanto preservavam a compatibilidade total com versões anteriores, mas não preservavam a compatibilidade de migração total : o código antigo (anterior ao C # 2.0) é executado sem alterações no novo tempo de execução compatível com genéricos sem recompilação. Quanto à compatibilidade de migração , novas classes e interfaces de coleção genérica foram desenvolvidas para suplementar as coleções não genéricas do .NET 1.x, em vez de substituí-las. Além das interfaces de coleção genérica, as novas classes de coleção genérica implementam as interfaces de coleção não genérica onde possível. Isso evita o uso de novas coleções genéricas com métodos preexistentes (não genéricos), se esses métodos forem codificados para usar as classes de coleção .

Covariância e contravariância

A covariância e a contravariância são suportadas por ambos os idiomas. Java tem variação de site de uso que permite que uma única classe genérica declare membros usando co- e contravariância. C # tem variação de definição de site para interfaces genéricas e delegados. A variância não é suportada diretamente nas classes, mas é suportada por meio de sua implementação de interfaces variantes. C # também tem suporte de covariância de site de uso para métodos e delegados.

Programação funcional

Programação funcional Java C #
Referências de método sim sim
Fechamentos Todos os lambdas não introduzem um novo nível de escopo. Todas as variáveis ​​referenciadas devem ser efetivamente finais sim
Expressões lambda sim sim
Árvores de expressão Não sim
Linguagem de consulta genérica Não; mas veja 'Java Stream equivalente' (Monad) sim
Otimizações do compilador de recursão de cauda Não Apenas em x64

Fechamentos

Um encerramento é uma função embutida que captura variáveis ​​de seu escopo léxico.

C # oferece suporte a encerramentos como métodos anônimos ou expressões lambda com semântica de encerramento completa .

Em Java, as classes internas anônimas continuarão sendo a maneira preferida de emular encerramentos até que o Java 8 se torne o novo padrão. Esta é uma construção mais detalhada. Essa abordagem também tem algumas diferenças em comparação com fechamentos reais, notavelmente acesso mais controlado a variáveis ​​dos escopos de fechamento: apenas membros finais podem ser referenciados. Java 8, entretanto, apresenta lambdas que herdam totalmente o escopo atual e, de fato, não introduz um novo escopo.

Quando uma referência a um método pode ser passada para execução posterior, surge um problema sobre o que fazer quando o método tem referências a variáveis ​​/ parâmetros em seu escopo léxico. Os fechamentos C # podem acessar qualquer variável / parâmetro de seu escopo léxico. Nas classes internas anônimas do Java, apenas referências a membros finais do escopo lexical são permitidas, exigindo que o desenvolvedor marque quais variáveis ​​disponibilizar e em que estado (possivelmente exigindo boxing).

Lambdas e árvores de expressão

C # e Java apresentam um tipo especial de encerramentos in-line chamados lambdas . Esses são métodos anônimos: eles têm uma assinatura e um corpo, mas nenhum nome. Eles são usados ​​principalmente para especificar argumentos locais com valor de função em chamadas a outros métodos, uma técnica associada principalmente à programação funcional .

C #, ao contrário do Java, permite o uso de funções lambda como uma forma de definir estruturas de dados especiais chamadas árvores de expressão. Se eles são vistos como uma função executável ou como uma estrutura de dados depende da inferência do tipo do compilador e a que tipo de variável ou parâmetro eles são atribuídos ou lançados. Lambdas e árvores de expressão desempenham papéis importantes na Consulta Integrada à Linguagem (LINQ).

Metadados

Metadados Java C #
Anotações / atributos de metadados Baseado em interface; anotações definidas pelo usuário podem ser criadas Baseado em classe
Argumentos posicionais Não; a menos que um único argumento sim
Argumentos nomeados sim sim
Valores padrão Na definição Através da inicialização
Tipos aninhados sim sim
Especialização Não sim
Metadados condicionais Não sim

Pré-processamento, compilação e empacotamento

Pré-processamento , Compilação e Empacotamento Java C #
Namespaces Pacotes Namespaces
Conteúdo do arquivo Restrito Sem custos
Embalagem Pacote visibilidade pública / interna nos membros do namespace, que o sistema de compilação traduz em módulos e assemblies no nível do CLR
Caminho de pesquisa de classes / montagem ClassPath Tempo de compilação e tempo de execução
Compilação condicional Não; mas veja Apache Ant sim
Erros / avisos personalizados Sim; AnnotationProcessor sim
Regiões explícitas Não sim

Namespaces e conteúdo de arquivo

Em C #, os namespaces são semelhantes aos do C ++ . Ao contrário dos nomes de pacotes em Java, um namespace não está de forma alguma vinculado à localização do arquivo de origem. Embora não seja estritamente necessário para um local de arquivo de origem Java espelhar sua estrutura de diretório de pacote, é a organização convencional.

Ambas as linguagens permitem a importação de classes (por exemplo, import java.util.*em Java), permitindo que uma classe seja referenciada usando apenas seu nome. Às vezes, classes com o mesmo nome existem em vários namespaces ou pacotes. Essas classes podem ser referenciadas usando nomes totalmente qualificados ou importando apenas classes selecionadas com nomes diferentes. Para fazer isso, o Java permite importar uma única classe (por exemplo, import java.util.List). C # permite importar classes com um novo nome local usando a seguinte sintaxe: using Console = System.Console. Também permite importar especializações de classes na forma de . using IntList = System.Collections.Generic.List<int>

Ambas as linguagens têm uma sintaxe de importação estática que permite usar o nome curto de alguns ou todos os métodos / campos estáticos em uma classe (por exemplo, permitindo foo(bar)onde foo()podem ser importados estaticamente de outra classe). C # tem uma sintaxe de classe estática (não deve ser confundida com classes internas estáticas em Java), que restringe uma classe para conter apenas métodos estáticos. C # 3.0 introduz métodos de extensão para permitir que os usuários adicionem estaticamente um método a um tipo (por exemplo, permitindo foo.bar()onde bar()pode ser um método de extensão importado trabalhando no tipo de foo).

O compilador Java da Sun Microsystems requer que um nome de arquivo de origem corresponda à única classe pública dentro dele, enquanto C # permite várias classes públicas no mesmo arquivo e não impõe restrições ao nome do arquivo. C # 2.0 e posterior permite dividir uma definição de classe em vários arquivos usando a partialpalavra - chave no código-fonte. Em Java, uma classe pública sempre estará em seu próprio arquivo de origem. Em C #, os arquivos de código-fonte e a separação das unidades lógicas não estão estreitamente relacionados.

Compilação condicional

Ao contrário do Java, C # implementa compilação condicional usando diretivas de pré-processador . Ele também fornece um Conditional atributo para definir métodos que são chamados apenas quando uma determinada constante de compilação é definida. Dessa forma, as asserções podem ser fornecidas como um recurso do framework com o método Debug.Assert(), que só é avaliado quando a DEBUGconstante é definida. Desde a versão 1.4, Java fornece um recurso de linguagem para asserções, que são desativadas no tempo de execução por padrão, mas podem ser ativadas usando o switch -enableassertionsou -eaao invocar a JVM.

Recursos de threading e assíncronos

Ambas as linguagens incluem mecanismos de sincronização de thread como parte de sua sintaxe de linguagem.

Threading e sincronização Java C #
Tópicos sim sim
Grupo de discussão sim sim
Paralelismo baseado em tarefas sim sim
Semáforos sim sim
Monitores sim sim
Variáveis ​​locais de thread sim Sim; Classe ThreadStaticAttribute e ThreadLocal <T>

Paralelismo baseado em tarefas para C #

Com o .NET Framework 4.0, um novo modelo de programação baseado em tarefas foi introduzido para substituir o modelo assíncrono baseado em eventos existente. A API é baseada nas classes Taske Task<T>. As tarefas podem ser compostas e encadeadas.

Por convenção, todo método que retorna um Taskdeve ter seu nome pós-fixado com Async .

public static class SomeAsyncCode
{
    public static Task<XDocument> GetContentAsync()
    {
        HttpClient httpClient = new HttpClient();
        return httpClient.GetStringAsync("www.contoso.com").ContinueWith((task) => {
            string responseBodyAsText = task.Result;
            return XDocument.Parse(responseBodyAsText);
        });
    }
}

var t = SomeAsyncCode.GetContentAsync().ContinueWith((task) => {
    var xmlDocument = task.Result;
});

t.Start();

No C # 5, um conjunto de extensões de linguagem e compilador foi introduzido para facilitar o trabalho com o modelo de tarefa. Essas extensões de linguagem incluíam a noção de asyncmétodos e a awaitinstrução que faz o fluxo do programa parecer síncrono.

public static class SomeAsyncCode
{
    public static async Task<XDocument> GetContentAsync()
    {
        HttpClient httpClient = new HttpClient();
        string responseBodyAsText = await httpClient.GetStringAsync("www.contoso.com");
        return XDocument.Parse(responseBodyAsText);
    }
}

var xmlDocument = await SomeAsyncCode.GetContentAsync();

// The Task will be started on call with await.

A partir desse açúcar sintático, o compilador C # gera uma máquina de estado que lida com as continuações necessárias sem que os desenvolvedores tenham que pensar sobre isso.

Paralelismo baseado em tarefas para Java

Java oferece suporte a threads desde JDK 1.0. Java oferece alta versatilidade para a execução de threads, geralmente chamados de tarefas. Isso é feito implementando uma interface funcional (uma java.lang.Runnableinterface) definindo um único método void no-args conforme demonstrado no exemplo a seguir:

var myThread = new Thread(() -> {
    var threadName = Thread.currentThread().getName();
    System.out.println("Hello " + threadName);
});

myThread.start();

Semelhante ao C #, Java tem um mecanismo de nível superior para trabalhar com threads. Executorspode executar tarefas assíncronas e também gerenciar um grupo de subprocessos. Todos os threads de uma ExecutorServicesinstância são tratados em um pool . Esta ExecutorServiceinstância será reutilizada sob o capô para tarefas revenant, portanto, é possível executar quantas tarefas simultâneas o programador desejar durante o ciclo de vida do aplicativo usando uma única instância de serviço do executor.

Esta é a aparência do primeiro exemplo de thread usando executores:

ExecutorService executor = Executors.newSingleThreadExecutor();

executor.submit(() -> {
    var threadName = Thread.currentThread().getName();
    System.out.println("Hello " + threadName);
});

A ExecutorServiceinstância também oferece suporte a uma Callableinterface, outra interface de método único, Runnablemas a assinatura do método contido de Callableretorna um valor. Dessa forma, a expressão lambda também deve retornar um valor.

public static class SomeAsyncCode {

    ExecutorService executor = Executors.newSingleThreadExecutor();

    public static Future<String> getContentAsync(){
        return executor.submit(() -> {           
            HttpRequest httpReq = HttpRequest.newBuilder()
                .uri(new URI("https://www.graalvm.org"))
                .build();
            
            return HttpClient.newHttpClient()
                .send(httpReq, BodyHandlers.ofString())
                .body();
        });
    }

}

var webPageResult = SomeAsyncCode.getContentAsync().get();

Chamar o método get() bloqueia o thread atual e espera até que o chamável seja concluído antes de retornar o valor (no exemplo, o conteúdo de uma página da web):

Características adicionais

Aplicativos numéricos

Para apoiar adequadamente as aplicações no campo da computação matemática e financeira, existem vários recursos de linguagem.

A palavra-chave strictfp do Java permite cálculos estritos de ponto flutuante para uma região do código. Cálculos de ponto flutuante estritos exigem que, mesmo se uma plataforma oferecer maior precisão durante os cálculos, os resultados intermediários devem ser convertidos para simples / duplo. Isso garante que cálculos estritos de ponto flutuante retornem exatamente o mesmo resultado em todas as plataformas. Sem ponto flutuante estrito, uma implementação de plataforma é livre para usar maior precisão para resultados intermediários durante o cálculo. C # permite que uma implementação para uma determinada arquitetura de hardware sempre use uma precisão mais alta para resultados intermediários, se disponível, ou seja, C # não permite que o programador force resultados intermediários opcionalmente para usar a precisão menor potencial de simples / duplo.

Embora a aritmética de ponto flutuante de Java seja amplamente baseada no IEEE 754 (Padrão para Aritmética de Ponto Flutuante Binário), certos recursos não são suportados, mesmo ao usar o modificador strictfp, como Sinalizadores de Exceção e Arredondamentos Direcionados, habilidades exigidas pelo Padrão IEEE 754 (ver Críticas de Java, aritmética de ponto flutuante ).

C # fornece um tipo decimal integrado, que tem maior precisão (mas menos intervalo) do que Java / C # double. O tipo decimal é um tipo de dados de 128 bits adequado para cálculos financeiros e monetários. O tipo decimal pode representar valores que variam de 1,0 × 10 −28 a aproximadamente 7,9 × 10 28 com 28–29 dígitos significativos. A estrutura usa sobrecarga de operador C # para que decimais possam ser manipulados usando operadores como +, -, * e /, como outros tipos de dados primitivos.

Os tipos BigDecimale BigIntegerfornecidos com Java permitem a representação de precisão arbitrária de números decimais e números inteiros, respectivamente. A biblioteca padrão Java não possui classes para lidar com números complexos.

Os tipos BigInteger, e Complexfornecidos com C # permitem a representação e manipulação de números inteiros e complexos de precisão arbitrária, respectivamente. As estruturas usar C # operador sobrecarga de modo que os casos podem ser manipulados utilizando operadores tais como +, -, *, e /, tal como outros tipos de dados primitivos. A biblioteca padrão C # não tem classes para lidar com números de ponto flutuante de precisão arbitrária (consulte o software para aritmética de precisão arbitrária ).

C # pode ajudar aplicativos matemáticos com os operadores checkede uncheckedque permitem a ativação ou desativação da verificação em tempo de execução para estouro aritmético para uma região do código.

Consulta integrada de linguagem (LINQ)

A Consulta Integrada da Linguagem C # (LINQ) é um conjunto de recursos projetados para funcionar em conjunto para permitir habilidades de consulta na linguagem e é um recurso distinto entre C # e Java.

LINQ consiste nos seguintes recursos:

  • Os métodos de extensão permitem que interfaces ou classes existentes sejam estendidas com novos métodos. As implementações podem ser compartilhadas ou uma interface pode ter uma implementação dedicada.
  • Lambdas permitem a expressão de critérios de uma maneira funcional.
  • As árvores de expressão permitem que uma implementação específica capture um lambda como uma árvore de sintaxe abstrata em vez de um bloco executável. Isso pode ser utilizado por implementações para representar critérios em uma linguagem diferente, por exemplo, na forma de uma cláusula where de SQL, como é o caso de Linq, LINQ to SQL .
  • Tipos anônimos e inferência de tipo oferecem suporte para capturar e trabalhar com o tipo de resultado de uma consulta. Uma consulta pode unir-se e projetar-se em fontes de consulta que podem levar a um tipo de resultado que não pode ser nomeado.
  • Expressões de consulta para oferecer suporte a uma sintaxe familiar aos usuários SQL .
  • Tipos anuláveis ​​(levantados) para permitir uma melhor correspondência com provedores de consulta que suportam tipos anuláveis, como, por exemplo, SQL .

Interoperabilidade nativa

Interoperabilidade nativa Java C #
Interoperabilidade entre idiomas Sim (com GraalVM , Nashorn , CORBA , JNI ou JNA ) Sim; C # foi projetado para isso
Métodos externos / nativos sim sim
Marshalling Código de cola externo necessário Sim; metadados controlados
Ponteiros e aritmética Não; mas veja sun.misc.Unsafe sim
Tipos nativos sim sim
Buffers de tamanho fixo Não sim
Alocação de pilha explícita Não sim
Endereço de Não sim
Fixação de objeto (corrigir variável para endereço) Não sim
Ponteiros de função Não sim
Sindicatos Não sim

O recurso Java Native Interface (JNI) permite que programas Java chamem código não Java. No entanto, o JNI exige que o código que está sendo chamado siga várias convenções e impõe restrições aos tipos e nomes usados. Isso significa que uma camada de adaptação extra entre o código legado e Java é frequentemente necessária. Este código de adaptação deve ser codificado em uma linguagem não Java, geralmente C ou C ++. O Java Native Access (JNA) permite uma chamada mais fácil de código nativo que requer apenas a escrita de código Java, mas tem um custo de desempenho.

Além disso, bibliotecas de terceiros fornecem ponte Java- Component Object Model (COM), por exemplo, JACOB ( gratuito ) e J-Integra para COM ( proprietário ).

O .NET Platform Invoke ( P / Invoke ) oferece a mesma capacidade, permitindo chamadas de C # para o que a Microsoft chama de código não gerenciado . Por meio de atributos de metadados, o programador pode controlar exatamente como os parâmetros e resultados são organizados , evitando assim o código de cola externo necessário para o JNI equivalente em Java. P / Invoke permite acesso quase completo a APIs procedurais (como Win32 ou POSIX), mas acesso limitado a bibliotecas de classes C ++.

Além disso, o .NET Framework também fornece uma ponte .NET-COM, permitindo o acesso a componentes COM como se fossem objetos .NET de primeira classe.

C # também permite que o programador desabilite a verificação de tipo normal e outros recursos de segurança do CLR , o que permite o uso de variáveis de ponteiro . Ao usar este recurso, o programador deve marcar o código usando a unsafepalavra - chave. JNI, P / Invoke e código "inseguro" são recursos igualmente arriscados, expondo possíveis falhas de segurança e instabilidade do aplicativo. Uma vantagem do código gerenciado inseguro em relação ao P / Invoke ou JNI é que ele permite que o programador continue a trabalhar no ambiente C # familiar para realizar algumas tarefas que, de outra forma, exigiriam a chamada de código não gerenciado. Um assembly (programa ou biblioteca) usando código inseguro deve ser compilado com uma opção especial e será marcado como tal. Isso permite que os ambientes de tempo de execução tomem precauções especiais antes de executar códigos potencialmente perigosos.

Ambientes de tempo de execução

Java (a linguagem de programação) foi projetado para ser executado na plataforma Java por meio do Java Runtime Environment (JRE). A plataforma Java inclui a máquina virtual Java (JVM) e um conjunto comum de bibliotecas. O JRE foi originalmente projetado para suportar a execução interpretada com a compilação final como uma opção. A maioria dos ambientes JRE executa programas totalmente ou pelo menos parcialmente compilados, possivelmente com otimização adaptativa . O compilador Java produz bytecode Java . Na execução, o bytecode é carregado pelo Java runtime e interpretado diretamente ou compilado para instruções de máquina e então executado.

C # foi projetado para ser executado no Common Language Runtime (CLR). O CLR é projetado para executar código totalmente compilado. O compilador C # produz instruções de linguagem intermediária comum . Após a execução, o tempo de execução carrega esse código e compila as instruções da máquina na arquitetura de destino.

Exemplos

Entrada / saída

Exemplo que ilustra como copiar texto uma linha por vez de um arquivo para outro, usando os dois idiomas.

Java C #
import java.nio.file.*;

class FileIOTest {

    public static void main(String[] args) throws Exception {
        var lines = Files.readAllLines(Paths.get("input.txt"));
        Files.write(Paths.get("output.txt"), lines);
    }

}
using System.IO;

class FileIOTest
{
    public static void Main(string[] args)
    {
        var lines = File.ReadLines("input.txt");
        File.WriteAllLines("output.txt", lines);
    }
}
Notas sobre a implementação Java:
  • Files.readAllLineso método retorna uma Lista de String, com o conteúdo do arquivo de texto, Arquivos também tem o método readAllBytes, retorna um array de Strings.
  • Files.write método grava a matriz de bytes ou em um arquivo de saída, indicado por um objeto Path.
  • Files.write O método também cuida do armazenamento em buffer e do fechamento do fluxo de saída.
Notas sobre a implementação do C #:
  • O ReadLinesmétodo retorna um objeto enumerável que, após a enumeração, lerá o arquivo uma linha por vez.
  • O WriteAllLinesmétodo pega um enumerável e recupera uma linha por vez e o grava até que a enumeração termine.
  • O leitor subjacente alocará automaticamente um buffer, portanto, não há necessidade de introduzir explicitamente um fluxo em buffer.
  • WriteAllLines fecha automaticamente o fluxo de saída, também no caso de um encerramento anormal.

Integração de tipos definidos pela biblioteca

C # permite que tipos definidos por biblioteca sejam integrados com tipos e operadores existentes usando conversões implícitas / explícitas personalizadas e sobrecarga de operador, conforme ilustrado pelo exemplo a seguir:

Java C #
var bigNumber =
    new BigInteger("123456789012345678901234567890");

var answer = bigNumber.multiply(new BigInteger("42"));
var square = bigNumber.multiply(bigNumber);
var sum = bigNumber.add(bigNumber);
var bigNumber =
    BigInteger.Parse("123456789012345678901234567890");

var answer = bigNumber*42;
var square = bigNumber*bigNumber;
var sum = bigNumber + bigNumber;

Delegados C # e construções Java equivalentes

Java C #
    // a target class
    class Target {
        public boolean targetMethod(String arg) {
            // do something
            return true;
        }
    }

    // usage
    void doSomething() {
        // construct a target with the target method
        var target = new Target();

        // capture the method reference
        Function<String, Boolean> ivk = target::targetMethod;

        // invokes the referenced method
        boolean result = ivk.apply("argumentstring");
    }
    // a target class
    class Target
    {
        public bool TargetMethod(string arg)
        {
            // do something
            return true;
        }
    }

    // usage
    void DoSomething()
    {
        // construct a target with the target method
        var target = new Target();

        // capture the delegate for later invocation
        Func<string, bool> dlg = target.TargetMethod;

        // invoke the delegate
        bool result = dlg("argumentstring");
    }

Levantamento de tipo

Java C #

Java não tem esse recurso, embora um efeito semelhante seja possível com a Optionalclasse

var a = Optional.of(42);
var b = Optional.empty();

// orElse(0) returns 0 if the value of b is null
var c = a.get() * b.orElse(0);
int? a = 42;
int? b = null;

// c will receive the null value
// because*is lifted and one of the operands are null
int? c = a * b;

Interoperabilidade com linguagens dinâmicas

Este exemplo ilustra como Java e C # podem ser usados ​​para criar e invocar uma instância de classe que é implementada em outra linguagem de programação. A classe "Deepthought" é implementada usando a linguagem de programação Ruby e representa uma calculadora simples que multiplicará dois valores de entrada ( ae b) quando o Calculatemétodo for invocado. Além da forma convencional, o Java possui o GraalVM , uma máquina virtual capaz de rodar qualquer linguagem de programação implementada.

Java C #

Usando GraalVM

Context polyglot = Context.newBuilder().allowAllAccess(true).build();

//Ruby
Value rubyArray = polyglot.eval("ruby", "[1,2,42,4]");
int rubyResult = rubyArray.getArrayElement(2).asInt();

//Python
Value pythonArray = context.eval("python", "[1,2,42,4]");
int pythonResult = pythonArray.getArrayElement(2).asInt();

//JavaScript
Value jsArray = polyglot.eval("js", "[1,2,42,4]");
int jsResult = jsArray.getArrayElement(2).asInt();

//R
Value rArray = polyglot.eval("R", "c(1,2,42,4)");
int rResult = rArray.getArrayElement(2).asInt();

//LLVM (in this case C, but could be C++, Go, Basic, etc...)
Source source = Source.newBuilder("llvm", new File("C_Program.bc")).build();
Value cpart = polyglot.eval(source);
cpart.getMember("main").execute();

Caminho tradicional

// initialize the engine
var invocable = new ScriptEngineManager().getEngineByName("jruby");
var rubyFile = new FileReader("Deepthought.rb");
engine.eval(fr);
// initialize the engine

var runtime = ScriptRuntime.CreateFromConfiguration();
dynamic globals = runtime.Globals;

runtime.ExecuteFile("Deepthought.rb");
// create a new instance of "Deepthought" calculator
var calc = globals.Deepthought.@new();

// set calculator input values
calc.a = 6;
calc.b = 7;

// calculate the result
var answer = calc.Calculate();
// create a new instance of "Deepthought" calculator
var calcClass = engine.eval("Deepthought");
var calc = invocable.invokeMethod(calcClass, "new");

// set calculator input values
invocable.invokeMethod(calc, "a=", 6);
invocable.invokeMethod(calc, "b=", 7);

// calculate the result
var answer = invocable.invokeMethod(calc, "Calculate");

Notas para a implementação Java:

  • Os nomes dos acessadores Ruby são gerados a partir do nome do atributo com um =sufixo. Ao atribuir valores, os desenvolvedores Java devem usar o nome do método acessador Ruby.
  • Objetos dinâmicos de um idioma estrangeiro não são objetos de primeira classe, pois devem ser manipulados por meio de uma API.

Notas para a implementação C #:

  • Os objetos retornados de propriedades ou métodos de dynamicobjetos são eles próprios do dynamictipo. Quando a inferência de tipo (a varpalavra - chave) é usada, as variáveis ​​calc e answer são deduzidas dynamic / late-bound.
  • Objetos dinâmicos de limites tardios são cidadãos de primeira classe que podem ser manipulados usando a sintaxe C #, embora tenham sido criados por uma linguagem externa.
  • newé uma palavra reservada. O @prefixo permite que palavras-chave sejam usadas como identificadores.

Sequência de Fibonacci

Este exemplo ilustra como a sequência de Fibonacci pode ser implementada usando as duas linguagens. A versão C # aproveita os métodos do gerador C # . A versão Java tira proveito das Streamreferências de interface e método. Os exemplos Java e C # usam o estilo K&R para a formatação de código de classes, métodos e instruções.

Java C #
// The Fibonacci sequence
Stream.generate(new Supplier<Integer>() {
    int a = 0;
    int b = 1;

    public Integer get() {
        int temp = a;
        a = b;
        b = a + temp;
        return temp;
    }
}).limit(10).forEach(System.out::println);
// The Fibonacci sequence
public IEnumerable<int> Fibonacci() 
{
    int a = 0;
    int b = 1;

    while (true) 
    {
        yield return a;
        (a, b) = (b, a + b);
    }
}
// print the 10 first Fibonacci numbers
foreach (var it in Fibonacci().Take(10)) 
{
    Console.WriteLine(it);
}
Observações para a versão Java:
  • A interface Java 8 Stream é uma sequência de elementos que suportam operações agregadas sequenciais e paralelas.
  • O método generate retorna um fluxo não ordenado sequencial infinito onde cada elemento é gerado pelo fornecedor fornecido.
  • O método limit retorna um fluxo que consiste nos elementos deste fluxo, truncado para não ter mais que maxSize de comprimento.
  • O método forEach executa uma ação para cada elemento deste fluxo, esta ação pode ser um lambda ou uma referência de método.

Funcional com estilo de API de fluxo

O algoritmo acima pode ser escrito de forma ainda mais consistente, usando Stream.iterate. O método iterate recebe um parâmetro seed e uma função que especifica o que fazer para cada iteração. Nesse caso, a semente pode ser uma classe de registro com os 2 valores iniciais do algoritmo, e sua respectiva transformação a cada iteração.

record Pair(int x, int y);

Stream.iterate(new Pair(0,1), p -> new Pair(p.y(), p.x() + p.y()))
    .limit(10)
    .map(p -> p.x())
    .forEach(System.out::println);
Notas para a versão C #:
  • O método é definido como instâncias de retorno da interface IEnumerable<int>, o que permite que o código do cliente solicite repetidamente o próximo número de uma sequência.
  • A yieldpalavra-chave converte o método em um método gerador.
  • A yield returninstrução retorna o próximo número da sequência e cria uma continuação para que as invocações subsequentes IEnumerabledo MoveNextmétodo da interface continuem a execução a partir da instrução seguinte com todas as variáveis ​​locais intactas.
  • A atribuição de tupla evita a necessidade de criar e usar uma variável temporária ao atualizar os valores das variáveis ae b.

Veja também

Referências

links externos