Uma análise aprofundada de C++ vs. Java

Publicados: 2022-07-22

Inúmeros artigos comparam os recursos técnicos do C++ e do Java, mas quais diferenças são mais importantes a serem consideradas? Quando uma comparação mostra, por exemplo, que Java não suporta herança múltipla e C++ sim, o que isso significa? E é uma coisa boa? Alguns argumentam que isso é uma vantagem do Java, enquanto outros o declaram um problema.

Vamos explorar as situações em que os desenvolvedores devem escolher C++, Java ou outra linguagem – e, ainda mais importante, por que a decisão é importante.

Examinando o básico: construções de linguagem e ecossistemas

O C++ foi lançado em 1985 como um front-end para compiladores C, semelhante a como o TypeScript compila para JavaScript. Compiladores C++ modernos normalmente compilam para código de máquina nativo. Embora alguns afirmem que os compiladores de C++ reduzem sua portabilidade e exigem reconstruções para novas arquiteturas de destino, o código C++ é executado em quase todas as plataformas de processador.

Lançado pela primeira vez em 1995, o Java não compila diretamente no código nativo. Em vez disso, Java constrói bytecode, uma representação binária intermediária que é executada na Java Virtual Machine (JVM). Em outras palavras, a saída do compilador Java precisa de um executável nativo específico da plataforma para ser executado.

Tanto C++ quanto Java se enquadram na família de linguagens semelhantes a C, pois geralmente se assemelham a C em sua sintaxe. A diferença mais significativa são seus ecossistemas: enquanto o C++ pode chamar perfeitamente bibliotecas baseadas em C ou C++, ou a API de um sistema operacional, o Java é mais adequado para bibliotecas baseadas em Java. Você pode acessar bibliotecas C em Java usando a API Java Native Interface (JNI), mas é propensa a erros e requer algum código C ou C++. O C++ também interage com o hardware mais facilmente do que o Java, pois o C++ é uma linguagem de nível inferior.

Trade-offs detalhados: genéricos, memória e mais

Podemos comparar C++ a Java de muitas perspectivas. Em alguns casos, a decisão entre C++ e Java é clara. Os aplicativos Android nativos normalmente devem usar Java, a menos que o aplicativo seja um jogo. A maioria dos desenvolvedores de jogos deve optar por C++ ou outra linguagem para obter a animação em tempo real mais suave possível; O gerenciamento de memória do Java geralmente causa atraso durante o jogo.

Aplicativos multiplataforma que não são jogos estão além do escopo desta discussão. Nem C++ nem Java são ideais neste caso porque são muito detalhados para o desenvolvimento de GUI eficiente. Para aplicativos de alto desempenho, é melhor criar módulos C++ para fazer o trabalho pesado e usar uma linguagem mais produtiva para o desenvolvedor para a GUI.

Aplicativos multiplataforma que não são jogos estão além do escopo desta discussão. Nem C++ nem Java são ideais neste caso porque são muito detalhados para o desenvolvimento de GUI eficiente.

Tweet

Para alguns projetos, a escolha pode não ser clara, então vamos comparar mais:

Característica C++ Java
Adequado para iniciantes Não Sim
Desempenho do tempo de execução Melhor Bom
Latência Previsível Imprevisível
Ponteiros inteligentes de contagem de referência Sim Não
Coleta de lixo global de marcação e varredura Não Requeridos
Alocação de memória de pilha Sim Não
Compilação para executável nativo Sim Não
Compilação para bytecode Java Não Sim
Interação direta com APIs de sistema operacional de baixo nível Sim Requer código C
Interação direta com bibliotecas C Sim Requer código C
Interação direta com bibliotecas Java Através do JNI Sim
Compilação padronizada e gerenciamento de pacotes Não Especialista


Além dos recursos comparados na tabela, também focaremos nos recursos de programação orientada a objetos (OOP), como herança múltipla, genéricos/modelos e reflexão. Observe que ambas as linguagens suportam OOP: Java exige isso, enquanto C++ suporta OOP juntamente com funções globais e dados estáticos.

Herança múltipla

Na OOP, herança é quando uma classe filha herda atributos e métodos de uma classe pai. Um exemplo padrão é uma classe Rectangle que herda de uma classe Shape mais genérica:

 // Note that we are in a C++ file class Shape { // Position int x, y; public: // The child class must override this pure virtual function virtual void draw() = 0; }; class Rectangle: public Shape { // Width and height int w, h; public: void draw(); };

Herança múltipla é quando uma classe filha herda de vários pais. Aqui está um exemplo, usando as classes Rectangle e Shape e uma classe adicional Clickable :

 // Not recommended class Shape {...}; class Rectangle: public Shape {...}; class Clickable { int xClick, yClick; public: virtual void click() = 0; }; class ClickableRectangle: public Rectangle, public Clickable { void click(); };

Neste caso temos dois tipos de base: Shape (o tipo base de Rectangle ) e Clickable . ClickableRectangle herda de ambos para compor os dois tipos de objeto.

C++ suporta herança múltipla; Java não. A herança múltipla é útil em certos casos extremos, como:

  • Criando uma linguagem avançada específica de domínio (DSL).
  • Executando cálculos sofisticados em tempo de compilação.
  • Melhorar a segurança do tipo de projeto de maneiras que simplesmente não são possíveis em Java.

No entanto, o uso de herança múltipla geralmente é desencorajado. Ele pode complicar o código e impactar o desempenho, a menos que seja combinado com a metaprogramação de template – algo melhor feito apenas pelos programadores C++ mais experientes.

Genéricos e Modelos

Versões genéricas de classes que funcionam com qualquer tipo de dados são práticas para reutilização de código. Ambas as linguagens oferecem esse suporte — Java por meio de genéricos, C++ por meio de modelos — mas a flexibilidade dos modelos C++ pode tornar a programação avançada mais segura e robusta. Os compiladores C++ criam novas classes ou funções personalizadas cada vez que você usa tipos diferentes com o modelo. Além disso, os modelos C++ podem chamar funções personalizadas com base nos tipos dos parâmetros da função de nível superior, permitindo que tipos de dados específicos tenham código especializado. Isso é chamado de especialização de modelo. Java não tem um recurso equivalente.

Em contraste, ao usar genéricos, compiladores Java criam objetos gerais sem tipos por meio de um processo chamado eliminação de tipos. Java executa a verificação de tipo durante a compilação, mas os programadores não podem modificar o comportamento de uma classe ou método genérico com base em seus parâmetros de tipo. Para entender isso melhor, vamos ver um exemplo rápido de uma função genérica std::string format(std::string fmt, T1 item1, T2 item2) que usa um template, template<class T1, class T2> , de um C++ biblioteca que criei:

 std::string firstParameter = "A string"; int secondParameter = 123; // Format printed output as an eight-character-wide string and a hexadecimal value format("%8s %x", firstParameter, secondParameter); // Format printed output as two eight-character-wide strings format("%8s %8s", firstParameter, secondParameter);

C++ produziria a função de format como std::string format(std::string fmt, std::string item1, int item2) , enquanto Java a criaria sem os tipos de objeto string e int específicos para item1 e item2 . Nesse caso, nosso modelo C++ sabe que o último parâmetro de entrada é um int e, portanto, pode realizar a conversão std::to_string necessária na segunda chamada de format . Sem modelos, uma instrução printf C++ tentando imprimir um número como uma string como na segunda chamada de format teria um comportamento indefinido e poderia travar o aplicativo ou imprimir lixo. A função Java só seria capaz de tratar um número como uma string na primeira chamada de format e não o formataria diretamente como um inteiro hexadecimal. Este é um exemplo trivial, mas demonstra a capacidade do C++ de selecionar um modelo especializado para manipular qualquer objeto de classe arbitrário sem modificar sua classe ou a função de format . Podemos produzir a saída corretamente em Java usando reflexão em vez de genéricos, embora esse método seja menos extensível e mais propenso a erros.

Reflexão

Em Java, é possível descobrir (em tempo de execução) detalhes estruturais como quais membros estão disponíveis em uma classe ou tipo de classe. Esse recurso é chamado de reflexão, provavelmente porque é como segurar um espelho no objeto para ver o que está dentro. (Mais informações podem ser encontradas na documentação de reflexão da Oracle.)

O C++ não tem reflexão completa, mas o C++ moderno oferece informações de tipo de tempo de execução (RTTI). O RTTI permite a detecção em tempo de execução de tipos de objetos específicos, embora não possa acessar informações como os membros do objeto.

Gerenciamento de memória

Outra diferença crítica entre C++ e Java é o gerenciamento de memória, que tem duas abordagens principais: manual, onde os desenvolvedores devem acompanhar e liberar a memória manualmente; e automático, onde o software rastreia quais objetos ainda estão em uso para reciclar a memória não utilizada. Em Java, um exemplo é a coleta de lixo.

Java requer memória coletada por lixo, fornecendo gerenciamento de memória mais fácil do que a abordagem manual e eliminando erros de liberação de memória que normalmente contribuem para vulnerabilidades de segurança. O C++ não fornece gerenciamento automático de memória nativamente, mas oferece suporte a uma forma de coleta de lixo chamada ponteiros inteligentes. Ponteiros inteligentes usam contagem de referência e são seguros e eficientes se usados ​​corretamente. C++ também oferece destruidores que limpam ou liberam recursos após a destruição de um objeto.

Enquanto Java oferece apenas alocação de heap, C++ suporta tanto alocação de heap (usando new e delete ou as funções C malloc mais antigas) quanto alocação de pilha. A alocação de pilha pode ser mais rápida e segura do que a alocação de heap porque uma pilha é uma estrutura de dados linear enquanto um heap é baseado em árvore, portanto, a memória de pilha é muito mais simples de alocar e liberar.

Outra vantagem do C++ relacionada à alocação de pilha é uma técnica de programação conhecida como Resource Acquisition Is Initialization (RAII). No RAII, recursos como referências estão vinculados ao ciclo de vida de seu objeto de controle; os recursos serão destruídos no final do ciclo de vida desse objeto. RAII é como os ponteiros inteligentes C++ funcionam sem desreferenciamento manual — um ponteiro inteligente referenciado na parte superior de uma função é automaticamente desreferenciado ao sair da função. A memória conectada também é liberada se esta for a última referência ao ponteiro inteligente. Embora o Java ofereça um padrão semelhante, é mais estranho que o RAII do C++, especialmente se você precisar criar vários recursos no mesmo bloco de código.

Desempenho do tempo de execução

Java tem um desempenho de tempo de execução sólido, mas o C++ ainda detém a coroa, pois o gerenciamento manual de memória é mais rápido do que a coleta de lixo para aplicativos do mundo real. Embora o Java possa superar o C++ em certos casos de canto devido à compilação JIT, o C++ vence a maioria dos casos não triviais.

Em particular, a biblioteca de memória padrão do Java sobrecarrega o coletor de lixo com suas alocações em comparação com o uso reduzido de alocações de heap do C++. No entanto, o Java ainda é relativamente rápido e deve ser aceitável, a menos que a latência seja uma das principais preocupações, por exemplo, em jogos ou aplicativos com restrições de tempo real.

Gerenciamento de compilação e pacote

O que o Java não tem em desempenho, compensa em facilidade de uso. Um componente que afeta a eficiência do desenvolvedor é o gerenciamento de compilação e pacote – como construímos projetos e trazemos dependências externas para um aplicativo. Em Java, uma ferramenta chamada Maven simplifica esse processo em algumas etapas fáceis e se integra a muitos IDEs, como o IntelliJ IDEA.

Em C++, entretanto, não existe um repositório de pacotes padronizado. Não existe nem mesmo um método padronizado para criar código C++ em aplicativos: alguns desenvolvedores preferem o Visual Studio, enquanto outros usam o CMake ou outro conjunto de ferramentas personalizado. Aumentando ainda mais a complexidade, certas bibliotecas C++ comerciais são formatadas em binário e não há uma maneira consistente de integrar essas bibliotecas ao processo de compilação. Além disso, variações nas configurações de compilação ou nas versões do compilador podem causar desafios para que as bibliotecas binárias funcionem.

Facilidade para iniciantes

O atrito de gerenciamento de compilação e pacote não é a única razão pela qual C++ é muito menos amigável para iniciantes do que Java. Um programador pode ter dificuldade em depurar e usar C++ com segurança, a menos que esteja familiarizado com C, linguagens assembly ou com o funcionamento de nível inferior de um computador. Pense no C++ como uma ferramenta poderosa: ele pode realizar muito, mas é perigoso se for mal utilizado.

A abordagem de gerenciamento de memória do Java acima mencionada também o torna muito mais acessível do que o C++. Os programadores Java não precisam se preocupar em liberar a memória do objeto, pois a linguagem cuida disso automaticamente.

Tempo de decisão: C++ ou Java?

Um fluxograma com uma bolha azul escura "Iniciar" no canto superior esquerdo que eventualmente se conecta a uma das sete caixas de conclusão azul claro abaixo dela, por meio de uma série de junções de decisão brancas com ramificações azuis escuras para "Sim" e outras opções, e ramos azuis claros para "Não". O primeiro é "aplicativo GUI multiplataforma?" do qual um "Sim" aponta para a conclusão: "Escolha um ambiente de desenvolvimento multiplataforma e use sua linguagem primária". Um "Não" aponta para "Aplicativo Android nativo?" a partir do qual um "Sim" aponta para uma pergunta secundária, "É um jogo?" Da pergunta secundária, um "Não" aponta para a conclusão, "Use Java (ou Kotlin)" e um "Sim" aponta para uma conclusão diferente, "Escolha um mecanismo de jogo multiplataforma e use sua linguagem recomendada". Do "Aplicativo Android Nativo?" pergunta, um "Não" aponta para "Aplicativo nativo do Windows?" a partir do qual um "Sim" aponta para uma pergunta secundária, "É um jogo?" Da pergunta secundária, um "Sim" aponta para a conclusão "Escolha um mecanismo de jogo multiplataforma e use seu idioma recomendado" e um "Não" aponta para uma conclusão diferente, "Escolha um ambiente de GUI do Windows e use seu linguagem (normalmente C++ ou C#)." Do "aplicativo nativo do Windows?" pergunta, um "Não" aponta para "Aplicativo de servidor?" da qual um "Sim" aponta para uma pergunta secundária, "Tipo de desenvolvedor?" Da pergunta secundária, uma decisão de "qualidade intermediária" aponta para a conclusão "Usar Java (ou C# ou TypeScript)" e uma decisão "qualificada" aponta para uma pergunta terciária "Prioridade máxima?" Da pergunta terciária, uma decisão "Produtividade do desenvolvedor" aponta para a conclusão "Usar Java (ou C# ou TypeScript)" e uma decisão "Desempenho" aponta para uma conclusão diferente, "Usar C++ (ou Rust)". Do "aplicativo do servidor?" pergunta, um "Não" aponta para uma pergunta secundária, "Desenvolvimento do driver?" Da pergunta secundária, um "Sim" aponta para uma conclusão, "Use C++ (ou Rust)" e um "Não" aponta para uma pergunta terciária, "Desenvolvimento de IoT?" Da pergunta terciária, "Sim" aponta para a conclusão, "Use C++ (ou Rust)", e um "Não" aponta para uma pergunta quaternária, "Negociação de alta velocidade?" Da pergunta quaternária, um "Sim" aponta para a conclusão, "Use C++ (ou Rust)" e um "Não" aponta para a conclusão final restante, "Pergunte a alguém familiarizado com seu domínio de destino".
Um guia expandido para escolher a melhor linguagem para vários tipos de projetos.

Agora que exploramos as diferenças entre C++ e Java em profundidade, voltamos à nossa pergunta original: C++ ou Java? Mesmo com uma compreensão profunda dos dois idiomas, não há uma resposta única.

Engenheiros de software não familiarizados com conceitos de programação de baixo nível podem se sair melhor selecionando Java ao restringir a decisão a C++ ou Java, exceto para contextos em tempo real, como jogos. Os desenvolvedores que desejam expandir seus horizontes, por outro lado, podem aprender mais escolhendo C++.

No entanto, as diferenças técnicas entre C++ e Java podem ser apenas um pequeno fator na decisão. Certos tipos de produtos exigem escolhas específicas. Se você ainda não tiver certeza, poderá consultar o fluxograma, mas lembre-se de que ele pode indicar um terceiro idioma.