Uno sguardo approfondito su C++ e Java

Pubblicato: 2022-07-22

Innumerevoli articoli confrontano le caratteristiche tecniche di C++ e Java, ma quali differenze sono più importanti da considerare? Quando un confronto mostra, ad esempio, che Java non supporta l'ereditarietà multipla e C++ lo fa, cosa significa? Ed è una buona cosa? Alcuni sostengono che questo sia un vantaggio di Java, mentre altri lo dichiarano un problema.

Esploriamo le situazioni in cui gli sviluppatori dovrebbero scegliere C++, Java o un altro linguaggio del tutto e, cosa ancora più importante, perché la decisione è importante.

Esaminare le basi: build linguistiche ed ecosistemi

C++ è stato lanciato nel 1985 come front-end per i compilatori C, in modo simile a come TypeScript compila in JavaScript. I moderni compilatori C++ in genere compilano in codice macchina nativo. Sebbene alcuni affermino che i compilatori di C++ ne riducano la portabilità e richiedano ricostruzioni per nuove architetture di destinazione, il codice C++ viene eseguito su quasi tutte le piattaforme di processore.

Rilasciato per la prima volta nel 1995, Java non si basa direttamente sul codice nativo. Invece, Java crea bytecode, una rappresentazione binaria intermedia che viene eseguita su Java Virtual Machine (JVM). In altre parole, l'output del compilatore Java necessita di un eseguibile nativo specifico della piattaforma per essere eseguito.

Sia C++ che Java rientrano nella famiglia dei linguaggi simili al C, poiché generalmente assomigliano al C nella loro sintassi. La differenza più significativa sono i loro ecosistemi: mentre C++ può chiamare senza problemi librerie basate su C o C++ o l'API di un sistema operativo, Java è più adatto per librerie basate su Java. È possibile accedere alle librerie C in Java utilizzando l'API JNI (Java Native Interface), ma è soggetta a errori e richiede del codice C o C++. C++ interagisce anche con l'hardware più facilmente di Java, poiché C++ è un linguaggio di livello inferiore.

Compromessi dettagliati: generici, memoria e altro

Possiamo confrontare C++ con Java da molte prospettive. In alcuni casi, la decisione tra C++ e Java è chiara. Le applicazioni Android native dovrebbero in genere utilizzare Java a meno che l'app non sia un gioco. La maggior parte degli sviluppatori di giochi dovrebbe optare per C++ o un altro linguaggio per l'animazione in tempo reale più fluida possibile; La gestione della memoria di Java causa spesso ritardi durante il gioco.

Le applicazioni multipiattaforma che non sono giochi esulano dallo scopo di questa discussione. Né C++ né Java sono l'ideale in questo caso perché sono troppo dettagliati per uno sviluppo efficiente della GUI. Per le app ad alte prestazioni, è meglio creare moduli C++ per fare il lavoro pesante e usare un linguaggio più produttivo per gli sviluppatori per la GUI.

Le applicazioni multipiattaforma che non sono giochi esulano dallo scopo di questa discussione. Né C++ né Java sono l'ideale in questo caso perché sono troppo dettagliati per uno sviluppo efficiente della GUI.

Twitta

Per alcuni progetti la scelta potrebbe non essere chiara, quindi confrontiamo ulteriormente:

Caratteristica C++ Giava
Adatto ai principianti No
Prestazioni di runtime Migliore Bene
Latenza Prevedibile Imprevedibile
Puntatori intelligenti per il conteggio dei riferimenti No
Raccolta globale dei rifiuti marcata e spazzata No Necessario
Allocazione della memoria dello stack No
Compilazione in eseguibile nativo No
Compilazione in bytecode Java No
Interazione diretta con le API del sistema operativo di basso livello Richiede il codice C
Interazione diretta con le librerie C Richiede il codice C
Interazione diretta con le librerie Java Attraverso JNI
Creazione standardizzata e gestione dei pacchetti No Esperto di


Oltre alle funzionalità confrontate nella tabella, ci concentreremo anche sulle funzionalità di programmazione orientata agli oggetti (OOP) come l'ereditarietà multipla, i generici/modelli e la riflessione. Nota che entrambi i linguaggi supportano OOP: Java lo richiede, mentre C++ supporta OOP insieme a funzioni globali e dati statici.

Ereditarietà multipla

In OOP, l'ereditarietà è quando una classe figlia eredita attributi e metodi da una classe padre. Un esempio standard è una classe Rectangle che eredita da una classe Shape più generica:

 // 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(); };

L'ereditarietà multipla è quando una classe figlio eredita da più genitori. Ecco un esempio, utilizzando le classi Rectangle e Shape e una classe Clickable aggiuntiva:

 // 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(); };

In questo caso abbiamo due tipi di base: Shape (il tipo di base di Rectangle ) e Clickable . ClickableRectangle eredita da entrambi per comporre i due tipi di oggetto.

C++ supporta l'ereditarietà multipla; Java no. L'ereditarietà multipla è utile in alcuni casi limite, come ad esempio:

  • Creazione di un linguaggio avanzato specifico del dominio (DSL).
  • Esecuzione di calcoli sofisticati in fase di compilazione.
  • Migliorare la sicurezza del tipo di progetto in modi che semplicemente non sono possibili in Java.

Tuttavia, l'utilizzo dell'ereditarietà multipla è generalmente sconsigliato. Può complicare il codice e influire sulle prestazioni a meno che non sia combinato con la metaprogrammazione dei modelli, cosa che viene eseguita al meglio solo dai programmatori C++ più esperti.

Generici e modelli

Le versioni generiche delle classi che funzionano con qualsiasi tipo di dati sono pratiche per il riutilizzo del codice. Entrambi i linguaggi offrono questo supporto, Java tramite i generics, C++ tramite i modelli, ma la flessibilità dei modelli C++ può rendere la programmazione avanzata più sicura e robusta. I compilatori C++ creano nuove classi o funzioni personalizzate ogni volta che si utilizzano tipi diversi con il modello. Inoltre, i modelli C++ possono chiamare funzioni personalizzate in base ai tipi dei parametri della funzione di primo livello, consentendo a determinati tipi di dati di avere codice specializzato. Questa è chiamata specializzazione del modello. Java non ha una funzionalità equivalente.

Al contrario, quando si utilizzano i generici, i compilatori Java creano oggetti generali senza tipi attraverso un processo chiamato cancellazione del tipo. Java esegue il controllo del tipo durante la compilazione, ma i programmatori non possono modificare il comportamento di una classe o di un metodo generico in base ai parametri del tipo. Per capirlo meglio, diamo un'occhiata a un rapido esempio di una funzione generica std::string format(std::string fmt, T1 item1, T2 item2) che usa un template, template<class T1, class T2> , da un C++ libreria che ho creato:

 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++ produrrebbe la funzione di format come std::string format(std::string fmt, std::string item1, int item2) , mentre Java la creerebbe senza i tipi di oggetto string e int specifici per item1 e item2 . In questo caso, il nostro modello C++ sa che l'ultimo parametro in arrivo è un int e quindi può eseguire la necessaria conversione std::to_string nella seconda chiamata di format . Senza i modelli, un'istruzione printf C++ che tenta di stampare un numero come una stringa come nella seconda chiamata di format avrebbe un comportamento indefinito e potrebbe causare l'arresto anomalo dell'applicazione o stampare la spazzatura. La funzione Java sarebbe in grado di trattare un numero solo come una stringa nella prima chiamata di format e non lo formatterebbe direttamente come intero esadecimale. Questo è un esempio banale, ma dimostra la capacità di C++ di selezionare un modello specializzato per gestire qualsiasi oggetto di classe arbitrario senza modificare la sua classe o la funzione di format . Possiamo produrre l'output correttamente in Java usando la riflessione invece dei generici, sebbene questo metodo sia meno estensibile e più soggetto a errori.

Riflessione

In Java, è possibile scoprire (in fase di esecuzione) dettagli strutturali come quali membri sono disponibili in una classe o in un tipo di classe. Questa caratteristica è chiamata riflessione, presumibilmente perché è come tenere uno specchio in alto sull'oggetto per vedere cosa c'è dentro. (Ulteriori informazioni possono essere trovate nella documentazione di riflessione di Oracle.)

C++ non ha una riflessione completa, ma il moderno C++ offre informazioni sul tipo di runtime (RTTI). RTTI consente il rilevamento di runtime di tipi di oggetti specifici, sebbene non possa accedere a informazioni come i membri dell'oggetto.

Gestione della memoria

Un'altra differenza fondamentale tra C++ e Java è la gestione della memoria, che ha due approcci principali: manuale, in cui gli sviluppatori devono tenere traccia e rilasciare la memoria manualmente; e automatico, in cui il software tiene traccia degli oggetti ancora in uso per riciclare la memoria inutilizzata. In Java, un esempio è la raccolta dei rifiuti.

Java richiede memoria raccolta dati inutili, fornendo una gestione della memoria più semplice rispetto all'approccio manuale ed eliminando gli errori di rilascio della memoria che comunemente contribuiscono alle vulnerabilità della sicurezza. C++ non fornisce la gestione automatica della memoria in modo nativo, ma supporta una forma di Garbage Collection chiamata puntatori intelligenti. I puntatori intelligenti utilizzano il conteggio dei riferimenti e sono sicuri e performanti se usati correttamente. C++ offre anche distruttori che ripuliscono o rilasciano risorse alla distruzione di un oggetto.

Mentre Java offre solo l'allocazione dell'heap, C++ supporta sia l'allocazione dell'heap (usando le funzioni new ed delete o le vecchie funzioni C malloc ) sia l'allocazione dello stack. L'allocazione dello stack può essere più rapida e sicura dell'allocazione dell'heap perché uno stack è una struttura di dati lineare mentre un heap è basato su albero, quindi la memoria dello stack è molto più semplice da allocare e rilasciare.

Un altro vantaggio di C++ relativo all'allocazione dello stack è una tecnica di programmazione nota come Resource Acquisition Is Initialization (RAII). In RAII, risorse come i riferimenti si legano al ciclo di vita del loro oggetto di controllo; le risorse verranno distrutte alla fine del ciclo di vita di quell'oggetto. RAII è il modo in cui funzionano i puntatori intelligenti C++ senza dereferenziazione manuale: un puntatore intelligente a cui si fa riferimento nella parte superiore di una funzione viene automaticamente dereferenziato all'uscita dalla funzione. La memoria collegata viene rilasciata anche se questo è l'ultimo riferimento al puntatore intelligente. Sebbene Java offra uno schema simile, è più imbarazzante di RAII di C++, soprattutto se è necessario creare più risorse nello stesso blocco di codice.

Prestazioni di runtime

Java ha solide prestazioni di runtime, ma C++ detiene ancora la corona poiché la gestione manuale della memoria è più veloce della garbage collection per le applicazioni del mondo reale. Sebbene Java possa superare C++ in alcuni casi d'angolo a causa della compilazione JIT, C++ vince la maggior parte dei casi non banali.

In particolare, la libreria di memoria standard di Java sovraccarica il Garbage Collector con le sue allocazioni rispetto all'uso ridotto di allocazioni di heap da parte di C++. Tuttavia, Java è ancora relativamente veloce e dovrebbe essere accettabile a meno che la latenza non sia una delle principali preoccupazioni, ad esempio nei giochi o nelle applicazioni con vincoli in tempo reale.

Creazione e gestione dei pacchetti

Ciò che manca a Java in termini di prestazioni, lo compensa con la facilità d'uso. Un componente che influisce sull'efficienza degli sviluppatori è la creazione e la gestione dei pacchetti: il modo in cui creiamo progetti e portiamo le dipendenze esterne in un'applicazione. In Java, uno strumento chiamato Maven semplifica questo processo in pochi semplici passaggi e si integra con molti IDE come IntelliJ IDEA.

In C++, tuttavia, non esiste un repository di pacchetti standardizzato. Non esiste nemmeno un metodo standardizzato per creare codice C++ nelle applicazioni: alcuni sviluppatori preferiscono Visual Studio, mentre altri usano CMake o un altro set personalizzato di strumenti. Aggiungendo ulteriormente alla complessità, alcune librerie C++ commerciali sono in formato binario e non esiste un modo coerente per integrare tali librerie nel processo di compilazione. Inoltre, le variazioni nelle impostazioni di compilazione o nelle versioni del compilatore possono causare difficoltà nel far funzionare le librerie binarie.

Cordialità per i principianti

L'attrito tra build e gestione dei pacchetti non è l'unico motivo per cui C++ è molto meno adatto ai principianti di Java. Un programmatore potrebbe avere difficoltà a eseguire il debug e a usare C++ in modo sicuro a meno che non abbia familiarità con C, linguaggi assembly o il funzionamento di livello inferiore di un computer. Pensa al C++ come a uno strumento potente: può fare molto ma è pericoloso se usato in modo improprio.

Il summenzionato approccio alla gestione della memoria di Java lo rende anche molto più accessibile rispetto a C++. I programmatori Java non devono preoccuparsi di rilasciare la memoria degli oggetti poiché il linguaggio se ne occupa automaticamente.

Tempo di decisione: C++ o Java?

Un diagramma di flusso con una bolla "Start" blu scuro nell'angolo in alto a sinistra che alla fine si collega a una delle sette caselle di conclusione azzurre sotto di essa, tramite una serie di giunzioni decisionali bianche con rami blu scuro per "Sì" e altre opzioni, e rami azzurri per "No". Il primo è "App GUI multipiattaforma?" da cui un "Sì" punta alla conclusione: "Scegli un ambiente di sviluppo multipiattaforma e usa il suo linguaggio principale". Un "No" indica "App Android nativa?" da cui un "Sì" indica una domanda secondaria, "È un gioco?" Dalla domanda secondaria, un "No" indica la conclusione "Usa Java (o Kotlin)" e un "Sì" indica una conclusione diversa, "Scegli un motore di gioco multipiattaforma e usa il suo linguaggio consigliato". Dall'"App Android nativa?" domanda, un "No" indica "App nativa di Windows?" da cui un "Sì" indica una domanda secondaria, "È un gioco?" Dalla domanda secondaria, un "Sì" indica la conclusione, "Scegli un motore di gioco multipiattaforma e usa il suo linguaggio consigliato" e un "No" indica una conclusione diversa, "Scegli un ambiente GUI di Windows e usa il suo linguaggio (in genere C++ o C#)." Da "App nativa di Windows?" domanda, un "No" indica "App server?" da cui un "Sì" punta a una domanda secondaria, "Tipo di sviluppatore?" Dalla domanda secondaria, una decisione "Mid-skill" punta alla conclusione "Usa Java (o C# o TypeScript)" e una decisione "Skilled" punta a una domanda terziaria, "Priorità massima?" Dalla domanda terziaria, una decisione "Produttività dello sviluppatore" punta alla conclusione "Usa Java (o C# o TypeScript)" e una decisione "Prestazioni" punta a una conclusione diversa, "Usa C++ (o Rust)". Dall'app "Server?" domanda, un "No" indica una domanda secondaria, "Sviluppo del driver?" Dalla domanda secondaria, un "Sì" indica una conclusione, "Usa C++ (o Rust)" e un "No" indica una domanda terziaria, "Sviluppo IoT?" Dalla domanda terziaria, "Sì" indica la conclusione "Usa C++ (o Rust)" e un "No" indica una domanda quaternaria, "Trading ad alta velocità?" Dalla domanda quaternaria, un "Sì" indica la conclusione, "Usa C++ (o Rust)" e un "No" indica l'ultima conclusione rimanente, "Chiedi a qualcuno che ha familiarità con il tuo dominio di destinazione".
Una guida estesa alla scelta della lingua migliore per vari tipi di progetto.

Ora che abbiamo esplorato in modo approfondito le differenze tra C++ e Java, torniamo alla nostra domanda originale: C++ o Java? Anche con una profonda comprensione delle due lingue, non esiste una risposta valida per tutti.

Gli ingegneri del software che non hanno familiarità con i concetti di programmazione di basso livello potrebbero scegliere Java quando si limita la decisione a C++ o Java, ad eccezione di contesti in tempo reale come i giochi. Gli sviluppatori che desiderano espandere i propri orizzonti, d'altra parte, potrebbero saperne di più scegliendo C++.

Tuttavia, le differenze tecniche tra C++ e Java potrebbero essere solo un piccolo fattore nella decisione. Alcune tipologie di prodotti richiedono scelte particolari. Se non sei ancora sicuro, puoi consultare il diagramma di flusso, ma tieni presente che alla fine potrebbe indirizzarti a una terza lingua.