Prezentare generală
în acest tutorial, vom arăta argumentele pro și contra de a folosi tipuri primitive Java și omologii lor învelite.
sistem de tip Java
Java are un sistem de tip dublu format din primitive precum int, boolean și tipuri de referință precum Integer, Boolean. Fiecare tip primitiv corespunde unui tip de referință.
fiecare obiect conține o singură valoare de tipul primitiv corespunzător. Clasele de înveliș sunt imuabile (astfel încât starea lor nu se poate schimba odată ce obiectul este construit) și sunt finale (astfel încât să nu putem moșteni de la ei).
sub capotă, Java efectuează o conversie între tipurile primitive și de referință dacă un tip real este diferit de cel declarat:
Integer j = 1; // autoboxingint i = new Integer(1); // unboxing
procesul de conversie a unui tip primitiv la unul de referință se numește autoboxing, procesul opus se numește unboxing.
Pro și contra
decizia ce obiect urmează să fie utilizat se bazează pe performanța aplicației pe care încercăm să o obținem, câtă memorie disponibilă avem, cantitatea de memorie disponibilă și ce valori implicite ar trebui să gestionăm.
dacă nu ne confruntăm cu niciunul dintre acestea, putem ignora aceste considerații, deși merită să le cunoaștem.
3.1. Amprenta de memorie singur element
doar pentru referință, variabilele de tip primitive au următorul impact asupra memoriei:
- boolean – 1 bit
- octet – 8 biți
- scurt, char – 16 biți
- int, float – 32 biți
- lung, dublu – 64 biți
în practică, aceste valori pot varia în funcție de punerea în aplicare mașină virtuală. În VM-ul Oracle, tipul boolean, de exemplu, este mapat la valorile int 0 și 1, deci este nevoie de 32 de biți, așa cum este descris aici: tipuri și valori Primitive.
variabilele de aceste tipuri trăiesc în stivă și, prin urmare, sunt accesate rapid. Pentru detalii, vă recomandăm tutorialul nostru despre modelul de memorie Java.
tipurile de referință sunt obiecte, trăiesc pe grămadă și sunt relativ lente de accesat. Ei au o anumită deasupra capului în ceea ce privește omologii lor primitivi.
Valorile concrete ale cheltuielilor generale sunt în general specifice JVM. Aici, vă prezentăm rezultatele pentru o mașină virtuală pe 64 de biți cu acești parametri:
java 10.0.1 2018-04-17Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)
pentru a obține structura internă a unui obiect, putem folosi Java Object Layout tool (consultați un alt tutorial despre cum să obțineți dimensiunea unui obiect).
se pare că o singură instanță a unui tip de referință pe acest JVM ocupă 128 biți, cu excepția lung și dublu care ocupă 192 biți:
- Boolean – 128 biți
- octet – 128 biți
- scurt, caracter – 128 biți
- întreg, Float – 128 biți
- lung, dublu – 192 biți
putem vedea că o singură variabilă de tip Boolean ocupă la fel de mult spațiu ca 128 primitive, în timp ce o variabilă întreagă ocupă la fel de mult spațiu ca patru int.
3.2. Amprenta de memorie pentru matrice
situația devine mai interesantă dacă comparăm cât de multă memorie ocupă matrice de tipurile luate în considerare.
când creăm matrice cu numărul diferit de elemente pentru fiecare tip, obținem un complot:
care demonstrează că tipurile sunt grupate în patru familii în ceea ce privește modul în care memoria m(s) depinde de numărul de elemente s ale matricei:
- lung, dublu: m(s) = 128 + 64 S
- scurt, char: m(s) = 128 + 64
- octet, Boolean: m(s) = 128 + 64
- restul: m ( s) = 128 + 64
unde parantezele pătrate denotă funcția standard a plafonului.în mod surprinzător, matricele tipurilor primitive long și double consumă mai multă memorie decât clasele lor de înveliș Long și Double.
putem vedea fie că matricele cu un singur element de tipuri primitive sunt aproape întotdeauna mai scumpe (cu excepția celor lungi și duble) decât tipul de referință corespunzător.
3.3. Performanță
performanța unui cod Java este o problemă destul de subtilă, depinde foarte mult de hardware-ul pe care rulează codul, de compilatorul care ar putea efectua anumite optimizări, de starea mașinii virtuale, de activitatea altor procese din sistemul de operare.așa cum am menționat deja, tipurile primitive trăiesc în stivă, în timp ce tipurile de referință trăiesc în grămadă. Acesta este un factor dominant care determină cât de repede obiectele obține fi accesate.
pentru a demonstra cât de mult operațiile pentru tipurile primitive sunt mai rapide decât cele pentru clasele wrapper, să creăm o matrice de cinci milioane de elemente în care toate elementele sunt egale, cu excepția ultimului; apoi efectuăm o căutare pentru acel element:
while (!pivot.equals(elements)) { index++;}
și comparăm performanța acestei operații pentru cazul în care matricea conține variabile ale tipurilor primitive și pentru cazul în care conține obiecte ale tipurilor de referință.
folosim binecunoscutul instrument de benchmarking JMH (vezi tutorialul nostru despre cum să-l folosești), iar rezultatele operației de căutare pot fi rezumate în această diagramă:
chiar și pentru o operație atât de simplă, putem vedea că este nevoie de mai mult timp pentru a efectua operația pentru clasele de înveliș.
în cazul operațiilor mai complicate, cum ar fi însumarea, înmulțirea sau împărțirea, diferența de viteză ar putea crește.
3.4. Valorile implicite
Valorile implicite ale tipurilor primitive sunt 0 (în reprezentarea corespunzătoare, adică 0, 0.0d etc) pentru tipurile numerice, false pentru tipul boolean, \u0000 pentru tipul char. Pentru clasele wrapper, valoarea implicită este null.
înseamnă că tipurile primitive pot dobândi valori numai din domeniile lor, în timp ce tipurile de referință ar putea dobândi o valoare (null) care într-un anumit sens nu aparține domeniilor lor.
deși nu este considerată o bună practică să lăsăm variabilele neinițializate, uneori am putea atribui o valoare după crearea sa.
într-o astfel de situație, când o variabilă de tip primitiv are o valoare egală cu cea implicită de tip, ar trebui să aflăm dacă variabila a fost într-adevăr inițializată.
nu există o astfel de problemă cu variabilele clasei de înfășurare, deoarece valoarea nulă este o indicație evidentă că variabila nu a fost inițializată.
utilizare
după cum am văzut, tipurile primitive sunt mult mai rapide și necesită mult mai puțină memorie. Prin urmare, am putea dori să preferăm să le folosim.
pe de altă parte, specificația actuală a limbajului Java nu permite utilizarea tipurilor primitive în tipurile parametrizate (generice), în colecțiile Java sau în API-ul de reflecție.
când aplicația noastră are nevoie de colecții cu un număr mare de elemente, ar trebui să luăm în considerare utilizarea matricelor cu un tip cât mai „economic” posibil, așa cum este ilustrat în graficul de mai sus.
concluzie
În acest tutorial, am ilustrat că obiectele din Java sunt mai lente și au un impact de memorie mai mare decât analogii lor primitivi.
ca întotdeauna, fragmente de cod pot fi găsite în depozitul nostru de pe GitHub.
începeți cu Spring 5 și Spring Boot 2, prin cursul Learn Spring:
>> verificați cursul