O privire aprofundată asupra C++ vs. Java
Publicat: 2022-07-22Nenumă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?
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ă.