O privire aprofundată asupra C++ vs. Java

Publicat: 2022-07-22

Nenumărate articole compară caracteristicile tehnice C++ și Java, dar care diferențe sunt cele mai importante de luat în considerare? Când o comparație arată, de exemplu, că Java nu acceptă moștenirea multiplă și C++, ce înseamnă asta? Și este un lucru bun? Unii susțin că acesta este un avantaj al Java, în timp ce alții îl declară o problemă.

Să explorăm situațiile în care dezvoltatorii ar trebui să aleagă C++, Java sau un alt limbaj cu totul – și, și mai important, de ce contează decizia.

Examinarea elementelor de bază: construcția limbajului și ecosistemele

C++ a fost lansat în 1985 ca un front-end pentru compilatoarele C, similar modului în care TypeScript se compilează în JavaScript. Compilatoarele C++ moderne se compilează de obicei în codul mașină nativ. Deși unii susțin că compilatoarele C++ îi reduc portabilitatea și necesită reconstruiri pentru noile arhitecturi țintă, codul C++ rulează pe aproape fiecare platformă de procesor.

Lansat pentru prima dată în 1995, Java nu se construiește direct pe codul nativ. În schimb, Java creează bytecode, o reprezentare binară intermediară care rulează pe Java Virtual Machine (JVM). Cu alte cuvinte, rezultatul compilatorului Java are nevoie de un executabil nativ specific platformei pentru a rula.

Atât C++, cât și Java se încadrează în familia limbajelor asemănătoare C, deoarece în general seamănă cu C în sintaxa lor. Cea mai semnificativă diferență este ecosistemele lor: în timp ce C++ poate apela fără probleme în biblioteci bazate pe C sau C++, sau API-ul unui sistem de operare, Java este cel mai potrivit pentru bibliotecile bazate pe Java. Puteți accesa bibliotecile C în Java folosind API-ul Java Native Interface (JNI), dar este predispus la erori și necesită ceva cod C sau C++. De asemenea, C++ interacționează cu hardware-ul mai ușor decât Java, deoarece C++ este un limbaj de nivel inferior.

Compensații detaliate: generice, memorie și multe altele

Putem compara C++ cu Java din multe perspective. În unele cazuri, decizia între C++ și Java este clară. Aplicațiile native Android ar trebui să folosească de obicei Java, cu excepția cazului în care aplicația este un joc. Majoritatea dezvoltatorilor de jocuri ar trebui să opteze pentru C++ sau un alt limbaj pentru o animație cât mai fluidă în timp real; Gestionarea memoriei Java cauzează adesea decalaj în timpul jocului.

Aplicațiile multi-platformă care nu sunt jocuri depășesc scopul acestei discuții. Nici C++, nici Java nu sunt ideale în acest caz, deoarece sunt prea detaliate pentru o dezvoltare eficientă a GUI. Pentru aplicațiile de înaltă performanță, cel mai bine este să creați module C++ pentru a face sarcini grele și să utilizați un limbaj mai productiv pentru dezvoltatori pentru GUI.

Aplicațiile multi-platformă care nu sunt jocuri depășesc scopul acestei discuții. Nici C++, nici Java nu sunt ideale în acest caz, deoarece sunt prea detaliate pentru o dezvoltare eficientă a GUI.

Tweet

Pentru unele proiecte, alegerea poate să nu fie clară, așa că să comparăm mai departe:

Caracteristică C++ Java
Prietenos pentru începători Nu da
Performanță de rulare Cel mai bun Bun
Latența Previzibil Imprevizibil
Indicatori inteligente de numărare a referințelor da Nu
Colectare globală a gunoiului cu marcare și măturare Nu Necesar
Alocarea memoriei stivei da Nu
Compilare la executabil nativ da Nu
Compilare la bytecode Java Nu da
Interacțiune directă cu API-urile sistemului de operare de nivel scăzut da Necesită cod C
Interacțiune directă cu bibliotecile C da Necesită cod C
Interacțiune directă cu bibliotecile Java Prin JNI da
Management standardizat de construcție și pachet Nu Maven


Pe lângă caracteristicile comparate în tabel, ne vom concentra și pe caracteristicile de programare orientată pe obiecte (OOP) precum moștenirea multiplă, generice/șabloane și reflecție. Rețineți că ambele limbi acceptă OOP: Java o impune, în timp ce C++ acceptă OOP alături de funcții globale și date statice.

Moștenirea multiplă

În OOP, moștenirea este atunci când o clasă copil moștenește atribute și metode de la o clasă părinte. Un exemplu standard este o clasă Rectangle care moștenește de la o clasă Shape mai generică:

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

Moștenirea multiplă este atunci când o clasă copil moștenește de la mai mulți părinți. Iată un exemplu, folosind clasele Rectangle și Shape și o clasă suplimentară 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(); };

În acest caz avem două tipuri de bază: Shape (tipul de bază al Rectangle ) și Clickable . ClickableRectangle moștenește de la ambele pentru a compune cele două tipuri de obiecte.

C++ acceptă moștenirea multiplă; Java nu. Moștenirea multiplă este utilă în anumite cazuri marginale, cum ar fi:

  • Crearea unui limbaj avansat specific domeniului (DSL).
  • Efectuarea de calcule sofisticate în timpul compilării.
  • Îmbunătățirea siguranței tipului de proiect în moduri care pur și simplu nu sunt posibile în Java.

Cu toate acestea, utilizarea moștenirii multiple este în general descurajată. Poate complica codul și poate avea impact asupra performanței, dacă nu este combinat cu metaprogramarea șablonului - lucru cel mai bine realizat doar de cei mai experimentați programatori C++.

Generice și șabloane

Versiunile generice ale claselor care funcționează cu orice tip de date sunt practice pentru reutilizarea codului. Ambele limbi oferă acest suport — Java prin generice, C++ prin șabloane — dar flexibilitatea șabloanelor C++ poate face programarea avansată mai sigură și mai robustă. Compilatoarele C++ creează noi clase sau funcții personalizate de fiecare dată când utilizați diferite tipuri cu șablonul. În plus, șabloanele C++ pot apela funcții personalizate pe baza tipurilor de parametri ai funcției de nivel superior, permițând anumitor tipuri de date să aibă cod specializat. Aceasta se numește specializare șablon. Java nu are o caracteristică echivalentă.

În schimb, atunci când folosesc generice, compilatoarele Java creează obiecte generale fără tipuri printr-un proces numit ștergere de tip. Java efectuează verificarea tipului în timpul compilării, dar programatorii nu pot modifica comportamentul unei clase sau metode generice pe baza parametrilor de tip. Pentru a înțelege mai bine acest lucru, să ne uităm la un exemplu rapid de funcție generică std::string format(std::string fmt, T1 item1, T2 item2) care utilizează un șablon, template<class T1, class T2> , dintr-un C++ biblioteca pe care am creat-o:

 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++ ar produce funcția de format ca std::string format(std::string fmt, std::string item1, int item2) , în timp ce Java ar crea-o fără tipurile specifice string și int obiect pentru item1 și item2 . În acest caz, șablonul nostru C++ știe că ultimul parametru primit este un int și, prin urmare, poate efectua conversia std::to_string în al doilea apel de format . Fără șabloane, o instrucțiune C++ printf care încearcă să imprime un număr ca șir ca în al doilea apel de format ar avea un comportament nedefinit și ar putea bloca aplicația sau ar putea imprima gunoi. Funcția Java ar putea trata un număr doar ca șir în primul apel de format și nu l-ar formata direct ca întreg hexazecimal. Acesta este un exemplu banal, dar demonstrează capacitatea C++ de a selecta un șablon specializat pentru a gestiona orice obiect de clasă arbitrară fără a-și modifica clasa sau funcția de format . Putem produce rezultatul corect în Java folosind reflectarea în loc de generice, deși această metodă este mai puțin extensibilă și mai predispusă la erori.

Reflecţie

În Java, este posibil să aflați (în timpul rulării) detalii structurale, cum ar fi ce membri sunt disponibili într-o clasă sau tip de clasă. Această caracteristică se numește reflexie, probabil pentru că este ca și cum ați ridica o oglindă spre obiect pentru a vedea ce este înăuntru. (Mai multe informații pot fi găsite în documentația de reflecție a Oracle.)

C++ nu are reflectare completă, dar C++ modern oferă informații despre tipul de rulare (RTTI). RTTI permite detectarea în timp de execuție a anumitor tipuri de obiecte, deși nu poate accesa informații precum membrii obiectului.

Gestionarea memoriei

O altă diferență critică între C++ și Java este gestionarea memoriei, care are două abordări majore: manuală, în care dezvoltatorii trebuie să țină evidența și să elibereze manual memoria; și automat, unde software-ul urmărește obiectele care sunt încă în uz pentru a recicla memoria neutilizată. În Java, un exemplu este colectarea gunoiului.

Java necesită memorie colectată de gunoi, oferind o gestionare mai ușoară a memoriei decât abordarea manuală și eliminând erorile de eliberare a memoriei care contribuie de obicei la vulnerabilitățile de securitate. C++ nu oferă management automat al memoriei în mod nativ, dar acceptă o formă de colectare a gunoiului numită pointeri inteligente. Indicatoarele inteligente folosesc contorizarea referințelor și sunt sigure și performante dacă sunt utilizate corect. C++ oferă, de asemenea, destructori care curăță sau eliberează resurse la distrugerea unui obiect.

În timp ce Java oferă doar alocare heap, C++ acceptă atât alocarea heap (folosind funcții new și delete sau mai vechi C malloc ), cât și alocarea stivei. Alocarea stivei poate fi mai rapidă și mai sigură decât alocarea stivei, deoarece o stivă este o structură de date liniară, în timp ce un heap este bazat pe arbore, astfel încât memoria stivei este mult mai ușor de alocat și eliberat.

Un alt avantaj al C++ legat de alocarea stivei este o tehnică de programare cunoscută sub numele de Resource Acquisition Is Initialization (RAII). În RAII, resurse precum referințele se leagă de ciclul de viață al obiectului lor de control; resursele vor fi distruse la sfârșitul ciclului de viață al obiectului respectiv. RAII este modul în care funcționează pointerii inteligente C++ fără dereferențiere manuală - un pointer inteligent referit în partea de sus a unei funcții este dereferențiat automat la ieșirea din funcție. Memoria conectată este de asemenea eliberată dacă aceasta este ultima referință la indicatorul inteligent. Deși Java oferă un model similar, este mai incomod decât RAII C++, mai ales dacă trebuie să creați mai multe resurse în același bloc de cod.

Performanță de rulare

Java are performanțe solide de rulare, dar C++ încă deține coroana, deoarece gestionarea manuală a memoriei este mai rapidă decât colectarea gunoiului pentru aplicațiile din lumea reală. Deși Java poate depăși C++ în anumite cazuri de colț datorită compilării JIT, C++ câștigă majoritatea cazurilor non-triviale.

În special, biblioteca de memorie standard a Java suprasolicită colectorul de gunoi cu alocările sale în comparație cu utilizarea redusă de către C++ a alocărilor heap. Cu toate acestea, Java este încă relativ rapid și ar trebui să fie acceptabil, cu excepția cazului în care latența este o preocupare principală - de exemplu, în jocuri sau aplicații cu constrângeri în timp real.

Gestionarea build-ului și a pachetelor

Ceea ce îi lipsește Java în performanță, îl compensează prin ușurința în utilizare. O componentă care afectează eficiența dezvoltatorului este gestionarea construcției și a pachetelor - modul în care construim proiecte și aducem dependențe externe într-o aplicație. În Java, un instrument numit Maven simplifică acest proces în câțiva pași simpli și se integrează cu multe IDE-uri, cum ar fi IntelliJ IDEA.

În C++, totuși, nu există un depozit de pachete standardizat. Nici măcar nu există o metodă standardizată de a construi cod C++ în aplicații: unii dezvoltatori preferă Visual Studio, în timp ce alții folosesc CMake sau un alt set personalizat de instrumente. Adăugând și mai mult la complexitate, anumite biblioteci comerciale C++ sunt formatate binar și nu există o modalitate consistentă de a integra acele biblioteci în procesul de construire. Mai mult, variațiile în setările de compilare sau versiunile compilatorului pot provoca provocări în a funcționa bibliotecile binare.

Amabilitate pentru începători

Fricțiunea de gestionare a construcției și a pachetelor nu este singurul motiv pentru care C++ este mult mai puțin prietenos pentru începători decât Java. Un programator poate avea dificultăți în depanarea și utilizarea C++ în siguranță, cu excepția cazului în care sunt familiarizați cu C, limbaje de asamblare sau funcționarea de nivel inferior a unui computer. Gândiți-vă la C++ ca la un instrument electric: poate realiza multe, dar este periculos dacă este folosit greșit.

Abordarea Java de gestionare a memoriei, menționată mai sus, o face, de asemenea, mult mai accesibilă decât C++. Programatorii Java nu trebuie să-și facă griji cu privire la eliberarea memoriei obiectelor, deoarece limbajul se ocupă automat de asta.

Timp de decizie: C++ sau Java?

O organigramă cu un balon de „Start” albastru închis în colțul din stânga sus, care în cele din urmă se conectează la una dintre cele șapte casete de concluzie albastru deschis de sub ea, printr-o serie de joncțiuni de decizie albe cu ramuri albastru închis pentru „Da” și alte opțiuni, și ramuri albastre deschis pentru „Nu”. Prima este „Aplicația GUI multiplatformă?” din care un „Da” indică concluzia „Alege un mediu de dezvoltare multiplatformă și folosește limbajul său principal”. Un „Nu” indică „Aplicație Android nativă?” din care un „Da” indică o întrebare secundară, „Este un joc?” Din întrebarea secundară, un „Nu” indică concluzia „Folosește Java (sau Kotlin)”, iar „Da” indică o concluzie diferită, „Alege un motor de joc multiplatform și folosește limbajul recomandat”. Din „Aplicația nativă Android?” întrebare, un „Nu” indică „aplicația Windows nativă?” din care un „Da” indică o întrebare secundară, „Este un joc?” Din întrebarea secundară, un „Da” indică concluzia „Alege un motor de joc multiplatform și folosește limba sa recomandată”, iar „Nu” indică o concluzie diferită, „Alege un mediu GUI Windows și folosește-l principal limbaj (de obicei C++ sau C#)." Din „aplicația Windows nativă?” întrebare, un „Nu” indică „Aplicația server?” din care un „Da” indică o întrebare secundară, „Tipul de dezvoltator?” Din întrebarea secundară, o decizie „Mijloc de calificare” indică concluzia „Utilizați Java (sau C# sau TypeScript)”, iar o decizie „Cu calificare” indică o întrebare terțiară, „Prioritate maximă?” Din întrebarea terțiară, o decizie „Productivitate dezvoltatorului” indică concluzia „Folosește Java (sau C# sau TypeScript)”, iar o decizie „Performanță” indică o concluzie diferită, „Folosește C++ (sau Rust)”. Din „Aplicația server?” întrebare, un „Nu” indică o întrebare secundară, „Driver development?” Din întrebarea secundară, un „Da” indică o concluzie, „Folosește C++ (sau Rust)”, iar un „Nu” indică o întrebare terțiară, „Dezvoltare IoT?” Din întrebarea terțiară, „Da” indică concluzia „Folosește C++ (sau Rust)”, iar „Nu” indică o întrebare cuaternară, „Tranzacționare de mare viteză?” Din întrebarea cuaternară, un „Da” indică concluzia „Folosește C++ (sau Rust)”, iar „Nu” indică concluzia finală rămasă, „Întreabă pe cineva familiarizat cu domeniul tău țintă”.
Un ghid extins pentru alegerea celei mai bune limbi pentru diferite tipuri de proiecte.

Acum că am explorat diferențele dintre C++ și Java în profunzime, revenim la întrebarea noastră inițială: C++ sau Java? Chiar și cu o înțelegere profundă a celor două limbi, nu există un răspuns unic.

Inginerii de software care nu sunt familiarizați cu conceptele de programare de nivel scăzut ar putea fi mai bine să selecteze Java atunci când restricționează decizia la C++ sau Java, cu excepția contextelor în timp real, cum ar fi jocurile. Pe de altă parte, dezvoltatorii care doresc să-și extindă orizonturile ar putea afla mai multe alegând C++.

Cu toate acestea, diferențele tehnice dintre C++ și Java pot fi doar un mic factor în decizie. Anumite tipuri de produse necesită alegeri speciale. Dacă încă nu sunteți sigur, puteți consulta diagrama, dar rețineți că, în cele din urmă, vă poate indica o a treia limbă.