
O artigo reflete a experiência pessoal do autor - um programador ávido de microcontroladores que, após muitos anos de experiência no desenvolvimento de microcontroladores em C (e um pouco em C ++), teve a oportunidade de participar de um grande projeto Java para desenvolver software para decodificadores de TV com Android. Durante esse projeto, pude coletar notas sobre diferenças interessantes entre as linguagens Java e C / C ++, avaliar diferentes abordagens para escrever programas. O artigo não pretende ser uma referência, mas não examina a eficiência e a produtividade dos programas Java. É antes uma coleção de observações pessoais. Salvo indicação em contrário, esta é uma versão do Java SE 7.
Diferenças de sintaxe e construções de controle
Em resumo - as diferenças são mínimas, a sintaxe é muito semelhante. Os blocos de código também são formados por um par de chaves {}. As regras para compilar identificadores são as mesmas que para C / C ++. A lista de palavras-chave é quase a mesma que em C / C ++. Tipos de dados internos - semelhantes aos do C / C ++. Matrizes - todas também são declaradas usando colchetes.
O controle constrói a opção if-else, while, do-while, for, também são quase completamente idênticas. Vale ressaltar que em Java havia rótulos familiares aos programadores C (aqueles que são usados com a palavra-chave goto e cujo uso é fortemente desencorajado). No entanto, Java excluiu a possibilidade de alternar para um rótulo usando goto. Os rótulos devem ser usados apenas para sair de loops aninhados:
outer: for (int i = 0; i < 5; i++) { inner: for (int j = 0; j < 5; j++) { if (i == 2) break inner; if (i == 3) continue outer; } }
Para melhorar a legibilidade dos programas em Java, foi adicionada uma oportunidade interessante para separar os dígitos de números longos com um sublinhado:
int value1 = 1_500_000; long value2 = 0xAA_BB_CC_DD;
Externamente, um programa Java não é muito diferente de um programa familiar C. A principal diferença visual é que o Java não permite funções, variáveis, definições de novos tipos (estruturas), constantes e assim por diante, localizadas "livremente" no arquivo de origem. Java é uma linguagem orientada a objetos, portanto, todas as entidades do programa devem pertencer a alguma classe. Outra diferença significativa é a falta de um pré-processador. Essas duas diferenças são descritas em mais detalhes abaixo.
Abordagem de objetos na linguagem C
Quando escrevemos grandes programas em C, basicamente temos que trabalhar com objetos. O papel do objeto aqui é desempenhado por uma estrutura que descreve uma certa essência do "mundo real":
Também em C existem métodos para processar "objetos" - estruturas - funções. No entanto, funções não são essencialmente mescladas com dados. Sim, eles geralmente são colocados em um arquivo, mas cada vez é necessário passar um ponteiro para o objeto a ser processado na função "típica":
int process(struct Data *ptr, int arg1, const char *arg2) { return result_code; }
Você pode usar o "objeto" somente após alocar memória para armazená-lo:
Data *data = malloc(sizeof(Data));
Em um programa C, geralmente é definida uma função responsável pela inicialização do "objeto" antes de seu primeiro uso:
void init(struct Data *data) { data->field = 1541; data->str = NULL; }
Então o ciclo de vida de um "objeto" em C é geralmente assim:
struct Data *data = malloc(sizeof(Data)); init(data); process(data, 0, "string"); free(data);
Agora, listamos os possíveis erros de tempo de execução que podem ser cometidos pelo programador no ciclo de vida do "objeto":
- Esqueça de alocar memória para o "objeto"
- Especifique a quantidade incorreta de memória alocada
- Esqueça de inicializar o "objeto"
- Esqueça de liberar memória depois de usar o objeto
Pode ser extremamente difícil detectar esses erros, pois eles não são detectados pelo compilador e aparecem durante a operação do programa. Além disso, seu efeito pode ser muito diversificado e afetar outras variáveis e "objetos" do programa.
Abordagem de objeto Java
Diante da OOP - programação orientada a objetos, você provavelmente já ouviu falar de uma das baleias OOP - encapsulamento. Em Java, diferentemente de C, dados e métodos para processá-los são combinados e são objetos "verdadeiros". Em termos de POO, isso é chamado de encapsulamento. Uma classe é uma descrição de um objeto, o análogo mais próximo de uma classe em C é definir um novo tipo usando typedef struct. Em termos de Java, as funções que pertencem a uma classe são chamadas métodos.
A ideologia da linguagem Java é baseada na afirmação "tudo é um objeto". Portanto, não é de surpreender que o Java proíba a criação de métodos (funções) e campos de dados (variáveis) separadamente da classe. Mesmo o método main () familiar, a partir do qual o programa é iniciado, deve pertencer a uma das classes.
Uma definição de classe em Java é análoga a uma declaração de estrutura em C. Ao descrever uma classe, você não cria nada na memória. Um objeto desta classe aparece no momento de sua criação pelo novo operador. Criar um objeto em Java é um análogo da alocação de memória na linguagem C, mas, diferentemente do último, um método especial é chamado automaticamente durante a criação do objeto - o construtor do objeto. O construtor assume o papel da inicialização inicial do objeto - um análogo da função init () discutida anteriormente. O nome do construtor deve corresponder ao nome da classe. O construtor não pode retornar um valor.
O ciclo de vida de um objeto em um programa Java é o seguinte:
Observe que o número de possíveis erros no programa Java é muito menor que no programa C. Sim, você ainda pode esquecer de criar o objeto antes do primeiro uso (o que, no entanto, levará a uma NullPointerException facilmente depurada), mas como nos outros erros inerentes Programas C, a situação está mudando fundamentalmente:
- Não há operador sizeof () em Java. O próprio compilador Java calcula a quantidade de memória para armazenar o objeto. Portanto, não é possível especificar o tamanho errado da seleção.
- A inicialização do objeto ocorre no momento da criação. É impossível esquecer a inicialização.
- A memória ocupada pelo objeto não precisa ser liberada; o coletor de lixo faz esse trabalho. É impossível esquecer de excluir um objeto após o uso - há menos probabilidade de um efeito de "vazamento de memória".
Portanto, tudo em Java é um objeto de uma classe ou de outra. As exceções são primitivas que foram adicionadas ao idioma para melhorar o desempenho e o consumo de memória. Mais sobre os primitivos está abaixo.
Coletor de memória e lixo
Java mantém os conceitos familiares de heap and stack para C / C ++, um programador. Ao criar um objeto com o novo operador, a memória para armazenar o objeto é emprestada da pilha. No entanto, um link para um objeto (um link é um análogo de um ponteiro), se o objeto criado não fizer parte de outro objeto, é colocado na pilha. Na pilha estão armazenados os "corpos" dos objetos e na pilha existem variáveis locais: referências a objetos e tipos primitivos. Se o heap existir durante a execução do programa e estiver disponível para todos os encadeamentos do programa, a pilha pertencerá ao método e só existirá durante sua execução e também não estará disponível para outros encadeamentos do programa.
Java é desnecessário e mais ainda - você não pode liberar manualmente a memória ocupada por um objeto. Este trabalho é realizado pelo coletor de lixo no modo automático. O tempo de execução monitora se é possível alcançar cada objeto no heap a partir do local atual do programa, seguindo os links de objeto para objeto. Caso contrário, esse objeto é reconhecido como "lixo" e se torna um candidato à exclusão.
É importante observar que a exclusão em si não ocorre no momento em que o objeto “não é mais necessário” - o coletor de lixo decide sobre a exclusão e a exclusão pode ser adiada o quanto for necessário até o término do programa.
Obviamente, o trabalho do coletor de lixo exige sobrecarga do processador. Mas, em troca, ele alivia o programador de uma grande dor de cabeça associada à necessidade de liberar memória após o término do uso de "objetos". De fato, “pegamos” a memória quando precisamos dela e a usamos, sem pensar que precisamos liberá-la depois de nós mesmos.
Falando sobre variáveis locais, devemos lembrar a abordagem de Java para sua inicialização. Se em C / C ++ uma variável local não inicializada contiver um valor aleatório, o compilador Java simplesmente não permitirá que ela seja deixada não inicializada:
int i;
Links - Ponteiros de substituição
Java não possui ponteiros; portanto, um programador Java não pode cometer um dos muitos erros que ocorrem ao trabalhar com ponteiros. Ao criar um objeto, você obtém um link para este objeto:
Em C, o programador teve uma escolha: como passar, digamos, uma estrutura para uma função. Você pode passar por valor:
A passagem por valor garantia que a função não alteraria os dados na estrutura, mas era ineficaz em termos de desempenho - no momento em que a função foi chamada, uma cópia da estrutura foi criada. Passar por um ponteiro é muito mais eficiente: de fato, o endereço na memória onde a estrutura está localizada foi passado para a função.
Em Java, havia apenas uma maneira de passar um objeto para um método - por referência. Passar por referência em Java é análogo a passar por um ponteiro em C:
- a cópia (clonagem) de memória não ocorre,
- de fato, o endereço da localização desse objeto é transmitido.
No entanto, diferentemente do ponteiro da linguagem C, um link Java não pode ser incrementado / decrementado. “Executar” os elementos de uma matriz usando um link para ela em Java não funcionará. Tudo o que pode ser feito com um link é atribuir um valor diferente.
Obviamente, a ausência de ponteiros, como tal, reduz o número de possíveis erros; no entanto, o análogo do ponteiro nulo permanece no idioma - uma referência nula indicada pela palavra-chave nula.
Uma referência nula é uma dor de cabeça para um programador Java, pois força a referência do objeto a ser verificada como nula antes de usá-la ou a manipular exceções NullPointerException. Se isso não for feito, o programa falhará.
Portanto, todos os objetos em Java são transmitidos por links. Tipos de dados primitivos (int, long, char ...) são passados por valor (mais sobre primitivos são fornecidos abaixo).
Recursos do Java Link
O acesso a qualquer objeto no programa é feito através de um link - isso claramente tem um efeito positivo no desempenho, mas pode surpreender um novato:
Argumentos de método e valores de retorno - tudo é passado através do link. Além das vantagens, há uma desvantagem em comparação com as linguagens C / C ++, onde podemos explicitamente proibir funções de alterar o valor passado por um ponteiro usando um qualificador const:
void func(const struct Data* data) {
Ou seja, a linguagem C permite rastrear esse erro no estágio de compilação. Java também possui a palavra-chave const, mas é reservada para versões futuras e atualmente não é usada. Até certo ponto, a palavra-chave final é chamada para cumprir seu papel. No entanto, ele não protege o objeto passado para o método contra alterações:
public class Main { void func(final Entity data) {
A questão é que a palavra-chave final, neste caso, é aplicada ao link, e não ao objeto para o qual o link aponta. Se final for aplicado à primitiva, o compilador se comportará conforme o esperado:
void func(final int value) {
Os links Java são muito semelhantes aos links da linguagem C ++.
Primitivas Java
Cada objeto Java, além dos campos de dados, contém informações de suporte. Se queremos operar, por exemplo, em bytes separados e cada byte é representado por um objeto, no caso de uma matriz de bytes, a sobrecarga de memória pode muitas vezes exceder o tamanho utilizável.
Para que o Java permaneça eficiente o suficiente nos casos descritos acima, o suporte para tipos primitivos - primitivos - foi adicionado à linguagem.
Primitivo | Ver | Profundidade de bits | Possível analógico em C |
---|
byte | Inteiro | 8 | char |
curto | 16 | curto |
char | 16 | wchar_t |
int | 32. | int (longo) |
longo | 64 | longo |
flutuar | Números de ponto flutuante | 32. | flutuar |
dobrar | | 64 | dobrar |
booleano | Logical | - | int (C89) / bool (C99) |
Todas as primitivas têm seus análogos na linguagem C. No entanto, o padrão C não determina o tamanho exato dos tipos inteiros; em vez disso, o intervalo de valores que esse tipo pode armazenar é fixo. Freqüentemente, o programador deseja garantir a mesma profundidade de bits para máquinas diferentes, o que leva ao aparecimento de tipos como uint32_t no programa, embora todas as funções da biblioteca exijam argumentos do tipo int.
Este fato não pode ser atribuído às vantagens da linguagem.
Primitivas inteiras Java, ao contrário de C, têm profundidades de bits fixas. Portanto, você não precisa se preocupar com a profundidade de bits real da máquina na qual o programa Java está sendo executado, nem com a ordem dos bytes ("rede" ou "Intel"). Esse fato ajuda a compreender o princípio "ele é escrito uma vez - é cumprido em toda parte".
Além disso, em Java, todas as primitivas inteiras são assinadas (o idioma não possui a palavra-chave não assinada). Isso elimina a dificuldade de usar variáveis assinadas e não assinadas em uma única expressão inerente a C.
Em conclusão, a ordem dos bytes nas primitivas de vários bytes em Java é fixa (byte baixo no endereço baixo, Little-endian, ordem reversa).
As desvantagens da implementação de operações com primitivas em Java incluem o fato de que aqui, como no programa C / C ++, o estouro da grade de bits pode ocorrer, sem exceções:
int i1 = 2_147_483_640; int i2 = 2_147_483_640; int r = (i1 + i2);
Portanto, os dados em Java são representados por dois tipos de entidades: objetos e primitivas. As primitivas violam o conceito de "tudo é um objeto", mas em algumas situações elas são eficazes demais para não usá-las.
Herança
A herança é outra baleia OOP da qual você provavelmente já ouviu falar. Se você responder brevemente à pergunta "por que a herança é necessária", a resposta será "reutilização de código".
Suponha que você programe em C e tenha uma "classe" bem escrita e depurada - uma estrutura e funções para processá-la. Em seguida, surge a necessidade de criar uma "classe" semelhante, mas com funcionalidade aprimorada, e a "classe" básica ainda é necessária. No caso da linguagem C, você tem apenas uma maneira de resolver esse problema - composição. Trata-se de criar uma nova estrutura estendida - "classe", que deve conter um ponteiro para a estrutura "classe" base:
struct Base { int field1; char *field2; }; void baseMethod(struct Base *obj, int arg); struct Extended { struct Base *base; int auxField; }; void extendedMethod(struct Extended *obj, int arg) { baseMethod(obj->base, 123); }
Java como uma linguagem orientada a objetos permite estender a funcionalidade de classes existentes usando o mecanismo de herança:
Deve-se notar que o Java de forma alguma proíbe o uso da composição como uma maneira de estender a funcionalidade de classes já escritas. Além disso, em muitas situações, a composição é preferível à herança.
Graças à herança, as classes em Java são organizadas em uma estrutura hierárquica, cada classe necessariamente tem um e apenas um "pai" e pode ter qualquer número de "filhos". Ao contrário do C ++, uma classe em Java não pode herdar de mais de um pai (isso resolve o problema da "herança de diamante").
Durante a herança, a classe derivada obtém em seu local todos os campos e métodos públicos e protegidos de sua classe base, bem como a classe base de sua classe base e assim por diante na hierarquia de herança.
No topo da hierarquia de herança está o progenitor comum de todas as classes Java - a classe Object, a única que não possui um pai.
Identificação dinâmica do tipo
Um dos pontos principais da linguagem Java é o suporte à identificação dinâmica de tipo (RTTI). Em palavras simples, o RTTI permite substituir um objeto de uma classe derivada em que é necessária uma referência à base:
Tendo um link em tempo de execução, é possível determinar o tipo verdadeiro do objeto ao qual o link se refere - usando o operador instanceof:
if (link instanceof Base) {
Substituições de método
Redefinir um método ou função significa substituir seu corpo no estágio de execução do programa. Os programadores C estão cientes da capacidade de uma linguagem de alterar o comportamento de uma função durante a execução do programa. É sobre o uso de ponteiros de função. Por exemplo, você pode incluir um ponteiro para uma função na estrutura da estrutura e atribuir várias funções ao ponteiro para alterar o algoritmo de processamento de dados dessa estrutura:
struct Object {
Em Java, como em outras linguagens OOP, a substituição de métodos está inextricavelmente vinculada à herança. Uma classe derivada obtém acesso aos métodos públicos e protegidos da classe base. Além do fato de ele poder chamá-los, você pode alterar o comportamento de um dos métodos da classe base sem alterar sua assinatura. Para fazer isso, basta definir um método com exatamente a mesma assinatura na classe derivada:
É muito importante que a assinatura (nome do método, valor de retorno, argumentos) corresponda exatamente. Se o nome do método corresponder e os argumentos diferirem, o método será sobrecarregado, mais sobre o que está abaixo.
Polimorfismo
Como o encapsulamento e a herança, a terceira baleia POO - polimorfismo - também possui algum tipo de análogo na linguagem C orientada a procedimentos.
Suponha que tenhamos várias "classes" de estruturas com as quais você deseja executar o mesmo tipo de ação, e a função que executa essa ação deve ser universal - deve "ser capaz" de trabalhar com qualquer "classe" como argumento.
Uma solução possível é a seguinte: enum Ids { ID_A, ID_B }; struct ClassA { int id; } void aInit(ClassA obj) { obj->id = ID_A; } struct ClassB { int id; } void bInit(ClassB obj) { obj->id = ID_B; } void commonFunc(void *klass) { int id = (int *)klass; switch (id) { case ID_A: ClassA *obj = (ClassA *) klass; break; case ID_B: ClassB *obj = (ClassB *) klass; break; } }
A solução parece complicada, mas o objetivo é alcançado - a função universal commonFunc () aceita o "objeto" de qualquer "classe" como argumento. Um pré-requisito é uma estrutura de "classe" no primeiro campo deve conter um identificador pelo qual a "classe" real do objeto é determinada. Essa solução é possível devido ao uso do argumento com o tipo "void *". No entanto, um ponteiro de qualquer tipo pode ser passado para essa função, por exemplo, "int *". Isso não causará erros de compilação, mas em tempo de execução, o programa se comportará de forma imprevisível.Agora vamos ver como o polimorfismo se parece em Java (no entanto, como em qualquer outra linguagem OOP). Suponha que temos muitas classes que devem ser processadas da mesma maneira por algum método. Ao contrário da solução para a linguagem C apresentada acima, esse método polimórfico DEVE ser incluído em todas as classes do conjunto fornecido e todas as suas versões DEVEM ter a mesma assinatura. class A { public void method() {} } class B { public void method() {} } class C { public void method() {} }
Em seguida, você precisa forçar o compilador a chamar exatamente a versão do método que pertence à classe correspondente. void executor(_set_of_class_ klass) { klass.method(); }
Ou seja, o método executor (), que pode estar em qualquer parte do programa, deve poder trabalhar com qualquer classe do conjunto (A, B ou C). De alguma forma, devemos "dizer" ao compilador que _set_of_class_ indica nossas muitas classes. Aqui a herança é útil - é necessário criar todas as classes a partir das derivadas definidas de alguma classe base, que conterão um método polimórfico: abstract class Base { abstract public void method(); } class A extends Base { public void method() {} } class B extends Base { public void method() {} } class C extends Base { public void method() {} } executor() : void executor(Base klass) { klass.method(); }
E agora qualquer classe que é herdeira de Base (graças à identificação dinâmica de tipo) pode ser passada a ela como argumento: executor(new A()); executor(new B()); executor(new C());
Dependendo de qual objeto de classe é passado como argumento, um método pertencente a essa classe será chamado.A palavra-chave abstract permite excluir o corpo do método (torná-lo abstrato, em termos de POO). De fato, estamos dizendo ao compilador que esse método deve ser substituído nas classes derivadas dele. Se não for esse o caso, ocorre um erro de compilação. Uma classe que contém pelo menos um método abstrato também é chamada de abstrata. O compilador exige marcar essas classes também com o resumo da palavra-chave.Estrutura de projeto Java
Em Java, todos os arquivos de origem têm a extensão * .java. Os arquivos de cabeçalho * .h e os protótipos de funções ou classes estão ausentes. Cada arquivo de origem Java deve conter pelo menos uma classe. O nome da classe é habitual para escrever, começando com uma letra maiúscula.Vários arquivos com código fonte podem ser combinados em um pacote. Para fazer isso, as seguintes condições devem ser atendidas:- Arquivos com código-fonte devem estar no mesmo diretório no sistema de arquivos.
- O nome deste diretório deve corresponder ao nome do pacote.
- No início de cada arquivo de origem, o pacote ao qual esse arquivo pertence deve ser indicado, por exemplo:
package com.company.pkg;
Para garantir a exclusividade dos nomes de pacotes no mundo, é proposto o uso do nome de domínio "invertido" da empresa. No entanto, isso não é um requisito e qualquer nome pode ser usado no projeto local.Também é recomendável que você especifique nomes de pacotes em minúsculas. Portanto, eles podem ser facilmente distinguidos dos nomes das classes.Ocultação da implementação
Outro aspecto do encapsulamento é a separação da interface e implementação. Se a interface estiver acessível para as partes externas do programa (externas ao módulo ou classe), a implementação estará oculta. Na literatura, uma analogia da caixa preta é frequentemente desenhada quando a implementação interna “não é visível” do lado de fora, mas o que é inserido na entrada da caixa e o que ele fornece é “visível”.Em C, ocultar implementações é realizada dentro de um módulo, marcando funções que não devem ser visíveis do lado de fora com a palavra-chave estática. Os protótipos das funções que compõem a interface do módulo são colocados no arquivo de cabeçalho. Um módulo em C significa um par: um arquivo de origem com a extensão *. Ce um cabeçalho com a extensão *. H.Java também possui a palavra-chave estática, mas não afeta a "visibilidade" do método ou campo de fora. Para controlar a “visibilidade”, existem 3 modificadores de acesso: privado, protegido e público.Os campos e métodos de uma classe marcada como privada estão disponíveis apenas dentro dela. Os campos e métodos protegidos também são acessíveis aos descendentes de classe. O modificador público significa que o elemento marcado é acessível de fora da classe, ou seja, faz parte da interface. Também é possível que não haja modificador; nesse caso, o acesso ao elemento de classe é limitado pelo pacote em que a classe está localizada.É recomendável que, ao escrever uma classe, marque inicialmente todos os campos da classe como privado e estenda os direitos de acesso conforme necessário.Sobrecarga de método
Um dos recursos irritantes da biblioteca padrão C é a presença de um zoológico inteiro de funções que executam essencialmente a mesma coisa, mas diferem no tipo de argumento, por exemplo: fabs (), fabsf (), fabsl () - funções para obter o valor absoluto de double, float e long tipos duplos, respectivamente.Java (assim como C ++) suporta um mecanismo de sobrecarga de método - pode haver vários métodos em uma classe com um nome completamente idêntico, mas diferentes no tipo e no número de argumentos. Pelo número de argumentos e seu tipo, o compilador escolherá a versão necessária do próprio método - é muito conveniente e melhora a legibilidade do programa.Em Java, diferentemente do C ++, os operadores não podem ser sobrecarregados. A exceção são os operadores "+" e "+ =", que são inicialmente sobrecarregados para as seqüências de caracteres String.Caracteres e seqüências de caracteres em Java
Em C, você precisa trabalhar com cadeias de terminal nulo representadas por ponteiros para o primeiro caractere: char *str;
Essas linhas devem terminar com um caractere nulo. Se for acidentalmente "apagado", uma sequência será considerada uma sequência de bytes na memória até o primeiro caractere nulo. Ou seja, se outras variáveis de programa forem colocadas na memória após a linha, depois de modificar uma linha danificada, seus valores poderão (e provavelmente serão) distorcidos.Obviamente, um programador C não é obrigado a usar seqüências de terminais nulos clássicas, mas aplica uma implementação de terceiros, mas aqui deve-se ter em mente que todas as funções da biblioteca padrão requerem sequências de terminais nulos como argumentos. Além disso, o padrão C não define a codificação usada, este ponto também deve ser controlado pelo programador.Em Java, o tipo de caractere primitivo (assim como o wrapper Character, sobre os wrappers abaixo) representa um único caractere de acordo com o padrão Unicode. A codificação UTF-16 é usada, respectivamente, um caractere ocupa 2 bytes na memória, o que permite codificar quase todos os caracteres dos idiomas usados atualmente.Os caracteres podem ser especificados por seu Unicode: char ch1 = '\u20BD';
Se o Unicode de um caractere exceder o máximo de 216 para char, esse caractere deverá ser representado por int. Na sequência, ele ocupará 2 caracteres de 16 bits, mas, novamente, caracteres com um código superior a 216 são usados raramente.As seqüências Java são implementadas pela classe String incorporada e armazenam caracteres char de 16 bits. A classe String contém tudo ou quase tudo o que pode ser necessário para trabalhar com seqüências de caracteres. Não há necessidade de pensar no fato de que a linha deve necessariamente terminar com zero; aqui é impossível imperceptivelmente "limpar" esse caractere de terminação zero ou acessar a memória além da linha. Em geral, ao trabalhar com seqüências de caracteres em Java, o programador não pensa em como a sequência é armazenada na memória.Como mencionado acima, Java não permite sobrecarga de operador (como em C ++), no entanto, a classe String é uma exceção - somente para ela os operadores de mesclagem de linha "+" e "+ =" são inicialmente sobrecarregados. String str1 = "Hello, " + "World!"; String str2 = "Hello, "; str2 += "World!";
Vale ressaltar que as strings em Java são imutáveis - uma vez criadas, elas não permitem sua alteração. Quando tentamos alterar a linha, por exemplo, assim: String str = "Hello, World!"; str.toUpperCase(); System.out.println(str);
Portanto, a string original não muda realmente. Em vez disso, é criada uma cópia modificada da sequência original, que por sua vez também é imutável: String str = "Hello, World!"; String str2 = str.toUpperCase(); System.out.println(str2);
Assim, cada alteração de uma string na realidade resulta na criação de um novo objeto (de fato, nos casos de mesclagem de strings, o compilador pode otimizar o código e usar a classe StringBuilder, que será discutida mais adiante).Acontece que o programa geralmente precisa mudar a mesma linha. Nesses casos, para otimizar a velocidade do programa e o consumo de memória, você pode impedir a criação de novos objetos de linha. Para esses propósitos, a classe StringBuilder deve ser usada: String sourceString = "Hello, World!"; StringBuilder builder = new StringBuilder(sourceString); builder.setCharAt(4, '0'); builder.setCharAt(8, '0'); builder.append("!!"); String changedString = builder.toString(); System.out.println(changedString);
Separadamente, vale a pena mencionar a comparação de strings. Um erro típico de um programador iniciante em Java é comparar cadeias usando o operador "==":
Esse código não contém formalmente erros no estágio de compilação ou erros de tempo de execução, mas funciona de maneira diferente do esperado. Como todos os objetos e cadeias, inclusive em Java, são representados por links, a comparação com o operador “==” fornece uma comparação de links, não valores de objetos. Ou seja, o resultado será verdadeiro apenas se 2 links realmente se referirem à mesma linha. Se as strings são objetos diferentes na memória e você precisa comparar seus conteúdos, use o método equals (): if (usersInput.equals("Yes")) {
O mais surpreendente é que, em alguns casos, a comparação usando o operador "==" funciona corretamente: String someString = "abc", anotherString = "abc";
Isso ocorre porque, na realidade, someString e anotherString se referem ao mesmo objeto na memória. O compilador coloca os mesmos literais de string no pool de strings - ocorre o chamado internamento. Então, toda vez que a mesma string literal aparece no programa, um link para a string do pool é usado. O internamento de strings é precisamente possível devido à propriedade de imutabilidade de strings.Embora a comparação do conteúdo de cadeias de caracteres seja permitida apenas pelo método equals (), em Java, é possível usar corretamente cadeias de caracteres em construções de casos de comutação (começando com Java 7): String str = new String();
Curiosamente, qualquer objeto Java pode ser convertido em uma string. O método toString () correspondente é definido na classe base para todas as classes da classe Object.Abordagem de tratamento de erros
Ao programar em C, você pode criar a seguinte abordagem de tratamento de erros. Cada função de uma biblioteca retorna um tipo int. Se a função for bem-sucedida, esse resultado será 0. Se o resultado for diferente de zero, isso indica um erro. Na maioria das vezes, o código de erro é passado pelo valor retornado pela função. Como a função pode retornar apenas um valor e já está ocupada pelo código de erro, o resultado real da função deve ser retornado através do argumento como um ponteiro, por exemplo, assim: int function(struct Data **result, const char *arg) { int errorCode; return errorCode; }
A propósito, este é um dos casos em que, em um programa C, torna-se necessário usar um ponteiro para um ponteiro.Às vezes, eles usam uma abordagem diferente. A função não retorna um código de erro, mas diretamente o resultado de sua execução, geralmente na forma de um ponteiro. Uma situação de erro é indicada com um ponteiro nulo. Em seguida, a biblioteca geralmente contém uma função separada que retorna o código do último erro: struct Data* function(const char *arg); int getLastError();
De uma maneira ou de outra, ao programar em C, o código que funciona “útil” e o código responsável pelo tratamento de erros se entrelaçam, o que obviamente não facilita a leitura do programa.Em Java, se desejar, você pode usar as abordagens descritas acima, mas aqui você pode aplicar uma maneira completamente diferente de lidar com erros - tratamento de exceções (no entanto, como em C ++). A vantagem do tratamento de exceções é que, nesse caso, o código “útil” e o código responsável pelo tratamento de erros e contingências são separados logicamente um do outro.Isso é conseguido usando construções try-catch: o código “útil” é colocado na seção try e o código de tratamento de erros é colocado na seção catch.
Há situações em que não é possível processar corretamente o erro no local de sua ocorrência. Nesses casos, uma indicação é colocada na assinatura do método de que o método pode causar esse tipo de exceção: public void func() throws Exception {
Agora, a chamada para esse método deve necessariamente ser enquadrada em um bloco try-catch, ou o método de chamada também deve ser marcado para que possa gerar essa exceção.Falta de pré-processador
Por mais conveniente que seja o pré-processador familiar aos programadores de C / C ++, ele está ausente na linguagem Java. Os desenvolvedores de Java provavelmente decidiram que ele é usado apenas para garantir a portabilidade dos programas e, como o Java roda em quase todos os lugares, um pré-processador não é necessário.Você pode compensar a falta de um pré-processador usando um campo de sinalizador estático e verificar seu valor no programa, quando necessário.Se estamos falando sobre a organização dos testes, é possível usar anotações em conjunto com reflexão (reflexão).Uma matriz também é um objeto.
Ao trabalhar com matrizes em C, a saída do índice além dos limites da matriz é um erro muito insidioso. O compilador não o reportará de forma alguma e, durante a execução, o programa não será parado com a mensagem correspondente: int array[5]; array[6] = 666;
Provavelmente, o programa continuará a execução, mas o valor da variável localizada após a matriz do exemplo acima será distorcida. Depurar esse tipo de erro pode não ser fácil.Em Java, o programador está protegido contra esse tipo de erros difíceis de diagnosticar. Quando você tenta ir além dos limites da matriz, uma ArrayIndexOutOfBoundsException é lançada. Se a captura de exceção não foi programada usando a construção try-catch, o programa trava e uma mensagem correspondente é enviada ao fluxo de erros padrão indicando o arquivo com o código-fonte e o número da linha em que a matriz foi excedida. Ou seja, o diagnóstico de tais erros se torna uma questão trivial.Esse comportamento do programa Java é possível porque a matriz em Java é representada por um objeto. A matriz Java não pode ser redimensionada; seu tamanho é codificado no momento em que a memória é alocada. Em tempo de execução, obter o tamanho da matriz é tão simples quanto isso: int[] array = new int[10]; int arraySize = array.length;
Se falamos de matrizes multidimensionais, comparado com a linguagem C, o Java oferece uma oportunidade interessante para organizar matrizes "ladder". Para o caso de uma matriz bidimensional, o tamanho de cada linha individual pode ser diferente do restante: int[][] array = new int[10][]; for (int i = 0; i < array.length; i++) { array[i] = new int[i + 1]; }
Como em C, os elementos da matriz estão localizados na memória, um por um, portanto, o acesso à matriz é considerado o mais eficiente. Se você precisar executar operações de inserção / exclusão de elementos ou criar estruturas de dados mais complexas, precisará usar coleções, como um conjunto (Conjunto), uma lista (Lista), um mapa (Mapa).Devido à falta de ponteiros e à incapacidade de incrementar links, o acesso aos elementos da matriz é possível usando índices.Colecções
Muitas vezes, a funcionalidade de matrizes não é suficiente - é necessário usar estruturas de dados dinâmicas. Como a biblioteca C padrão não contém uma implementação pronta de estruturas de dados dinâmicas, você deve usar a implementação em códigos-fonte ou na forma de bibliotecas.Diferentemente de C, a biblioteca Java padrão contém um rico conjunto de implementações de estruturas ou coleções dinâmicas de dados, expressas em termos de Java. Todas as coleções são divididas em três grandes classes: listas, conjuntos e mapas.Listas - matrizes dinâmicas - permitem adicionar / remover itens. Muitos não garantem a ordem dos elementos adicionados, mas garantem que não há elementos duplicados. Os cartões ou matrizes associativas operam com pares de valores-chave e o valor da chave é único - não pode haver 2 pares com as mesmas chaves no cartão.Para listas, conjuntos e mapas, há muitas implementações, cada uma delas otimizada para uma operação específica. Por exemplo, as listas são implementadas pelas classes ArrayList e LinkedList, com ArrayList fornecendo melhor desempenho ao acessar um elemento arbitrário, e o LinkedList é mais eficiente ao inserir / excluir elementos no meio da lista.Somente objetos Java completos podem ser armazenados em coleções (de fato, referências a objetos); portanto, é impossível criar uma coleção de primitivas diretamente (int, char, byte etc.). Nesse caso, as classes de wrapper apropriadas devem ser usadas:Primitivo | Classe de embalagem |
---|
byte | Byte |
curto | Curto |
char | Caráter |
int | Inteiro |
longo | Longo |
flutuar | Flutuar |
dobrar | Duplo |
booleano | Booleano |
Felizmente, ao programar em Java, não há necessidade de seguir a coincidência exata do tipo primitivo e seu "wrapper". Se o método receber um argumento, por exemplo, do tipo Inteiro, poderá ser passado o tipo int. E vice-versa, onde o tipo int é necessário, você pode usar com segurança o número inteiro. Isso foi possível graças ao mecanismo interno do Java para empacotar / descompactar primitivas.Dos momentos desagradáveis, deve-se mencionar que a biblioteca Java padrão contém classes de coleção antigas que foram implementadas sem êxito nas primeiras versões do Java e que não devem ser usadas em novos programas. Essas são as classes Enumeração, Vetor, Pilha, Dicionário, Hashtable, Propriedades.Generalizações
As coleções são comumente usadas como tipos de dados genéricos. A essência das generalizações nesse caso é que especificamos o tipo principal da coleção, por exemplo, ArrayList, e entre colchetes angulares especificamos o tipo de parâmetro, que neste caso determina o tipo de elementos armazenados na lista: List<Integer> list = new ArrayList<Integer>();
Isso permite que o compilador rastreie a tentativa de adicionar um objeto de um tipo diferente do parâmetro de tipo especificado: List<Integer> list = new ArrayList<Integer>();
É muito importante que o parâmetro type seja apagado durante a execução do programa, e não há diferença entre, por exemplo, um objeto da classe ArrayList <Integer>
e objeto de classe ArrayList <>.
Como resultado, não há como descobrir o tipo de elementos de coleção durante a execução do programa: public boolean containsInteger(List list) {
Uma solução parcial pode ser a seguinte abordagem: pegue o primeiro elemento da coleção e determine seu tipo: public boolean containsInteger(List list) { if (!list.isEmpty() && list.get(0) instanceof Integer) { return true; } return false; }
Mas essa abordagem não funcionará se a lista estiver vazia.Nesse sentido, as generalizações de Java são significativamente inferiores às generalizações de C ++. As generalizações Java realmente servem para "eliminar" alguns dos erros em potencial no estágio de compilação.Iterar sobre todos os elementos de uma matriz ou coleção
Ao programar em C, você geralmente precisa iterar sobre todos os elementos da matriz: for (int i = 0; i < SIZE; i++) { }
Cometer um erro aqui é mais simples, basta especificar o tamanho errado da matriz SIZE ou colocar "<=" em vez de "<".Em Java, além da forma “usual” da instrução for, existe uma forma de iterar sobre todos os elementos de uma matriz ou coleção (geralmente chamada foreach em outras linguagens): List<Integer> list = new ArrayList<>();
Aqui temos a garantia de iterar sobre todos os elementos da lista, os erros inerentes à forma "usual" da instrução for são eliminados.Coleções diversas
Como todos os objetos são herdados do objeto raiz, o Java tem uma oportunidade interessante de criar listas com vários tipos reais de elementos: List list = new ArrayList<>(); list.add(new String("First")); list.add(new Integer(2)); list.add(new Double(3.0)); instanceof: for (Object o : list) { if (o instanceof String) {
Transferências
Comparando C / C ++ e Java, é impossível não notar quanto mais enumerações funcionais são implementadas em Java. Aqui a enumeração é uma classe completa e os elementos de enumeração são objetos dessa classe. Isso permite que um elemento de enumeração defina vários campos de qualquer tipo em correspondência: enum Colors {
Como uma classe completa, uma enumeração pode ter métodos e, usando um construtor privado, você pode definir os valores do campo de elementos de enumeração individuais.Há uma oportunidade regular de obter uma representação em seqüência de caracteres de um elemento de enumeração, um número de série e uma matriz de todos os elementos: Colors color = Colors.BLACK; String str = color.toString();
E vice-versa - pela representação em cadeia, você pode obter um elemento de enumeração e também chamar seus métodos: Colors red = Colors.valueOf("RED");
Naturalmente, enumerações podem ser usadas em construções de casos de comutação.Conclusões
Obviamente, as linguagens C e Java são projetadas para resolver problemas completamente diferentes. Porém, se compararmos o processo de desenvolvimento de software nessas duas linguagens, de acordo com as impressões subjetivas do autor, a linguagem Java supera significativamente o C na conveniência e velocidade dos programas de gravação. O ambiente de desenvolvimento (IDE) desempenha um papel significativo no fornecimento de conveniência. O autor trabalhou com o IntelliJ IDEA IDE. Ao programar em Java, você não precisa "constantemente ter medo" de cometer um erro - geralmente o ambiente de desenvolvimento diz o que precisa ser corrigido e, às vezes, faz isso por você. Se um erro de tempo de execução ocorreu, o tipo de erro e o local de sua ocorrência no código-fonte são sempre indicados no log - a luta contra esses erros se torna uma questão trivial. Um programador C não precisa fazer esforços desumanos para mudar para Java, e tudo porque a sintaxe da linguagem mudou um pouco.Se essa experiência for interessante para os leitores, no próximo artigo, falaremos sobre a experiência do uso do mecanismo JNI (executando o código C / C ++ nativo de um aplicativo Java). O mecanismo JNI é indispensável quando você deseja controlar a resolução da tela, o módulo Bluetooth e, em outros casos, quando os recursos dos serviços e gerentes do Android não são suficientes.