Java-primitieven versus objecten

1. Overzicht

In deze zelfstudie laten we de voor- en nadelen zien van het gebruik van Java-primitieve typen en hun ingepakte tegenhangers.

2. Java-type systeem

Java heeft een tweevoudig type systeem dat bestaat uit primitieven zoals int, boolean en referentietypes zoals Geheel getal,Boolean. Elk primitief type komt overeen met een referentietype.

Elk object bevat een enkele waarde van het overeenkomstige primitieve type. De wrapper-klassen zijn onveranderlijk (zodat hun toestand niet kan veranderen als het object eenmaal is geconstrueerd) en definitief zijn (zodat we er niet van kunnen erven).

Onder de motorkap voert Java een conversie uit tussen het primitieve type en het referentietype als een feitelijk type verschilt van het gedeclareerde type:

Geheel getal j = 1; // autoboxing int i = new Integer (1); // uitpakken 

Het proces van het omzetten van een primitief type naar een referentietype wordt autoboxing genoemd, het tegenovergestelde proces wordt unboxing genoemd.

3. Voors en tegens

De beslissing welk object moet worden gebruikt, is gebaseerd op welke applicatieprestaties we proberen te bereiken, hoeveel beschikbaar geheugen we hebben, de hoeveelheid beschikbaar geheugen en welke standaardwaarden we moeten hanteren.

Als we met geen van deze overwegingen worden geconfronteerd, kunnen we deze overwegingen negeren, hoewel het de moeite waard is ze te kennen.

3.1. Geheugenvoetafdruk voor één item

Ter referentie: de variabelen van het primitieve type hebben de volgende impact op het geheugen:

  • boolean - 1 bit
  • byte - 8 bits
  • kort, char - 16 bits
  • int, float - 32 bits
  • lang, dubbel - 64 bits

In de praktijk kunnen deze waarden variëren, afhankelijk van de implementatie van de virtuele machine. In de VM van Oracle wordt het booleaanse type bijvoorbeeld toegewezen aan int-waarden 0 en 1, dus het duurt 32 bits, zoals hier wordt beschreven: Primitieve typen en waarden.

Variabelen van deze typen leven in de stapel en zijn daarom snel toegankelijk. Voor de details raden we onze tutorial over het Java-geheugenmodel aan.

De referentietypes zijn objecten, ze leven op de hoop en zijn relatief traag toegankelijk. Ze hebben een zekere overhead met betrekking tot hun primitieve tegenhangers.

De concrete waarden van de overhead zijn in het algemeen JVM-specifiek. Hier presenteren we resultaten voor een 64-bits virtuele machine met deze parameters:

java 10.0.1 2018-04-17 Java (TM) SE Runtime Environment 18.3 (build 10.0.1 + 10) Java HotSpot (TM) 64-bits server VM 18.3 (build 10.0.1 + 10, gemengde modus)

Om de interne structuur van een object te krijgen, kunnen we de Java Object Layout-tool gebruiken (zie onze andere tutorial over hoe je de grootte van een object kunt krijgen).

Het blijkt dat een enkele instantie van een referentietype op deze JVM 128 bits in beslag neemt, met uitzondering van Lang en Dubbele die 192 bits innemen:

  • Boolean - 128 bits
  • Byte - 128 bits
  • Kort, karakter - 128 bits
  • Geheel getal, Float - 128 bits
  • Lang, dubbel - 192 bits

We kunnen zien dat een enkele variabele van Boolean type neemt evenveel ruimte in als 128 primitieve, terwijl één Geheel getal variabele neemt maar liefst vier in beslag int degenen.

3.2. Geheugenvoetafdruk voor arrays

De situatie wordt interessanter als we vergelijken hoeveel geheugen arrays van de beschouwde typen in beslag nemen.

Wanneer we arrays maken met het verschillende aantal elementen voor elk type, krijgen we een plot:

dat toont aan dat de typen zijn gegroepeerd in vier families met betrekking tot hoe het geheugen Mevrouw) hangt af van het aantal elementen s van de array:

  • lang, dubbel: m (s) = 128 + 64 s
  • kort, char: m (s) = 128 + 64 [s / 4]
  • byte, boolean: m (s) = 128 + 64 [s / 8]
  • de rest: m (s) = 128 + 64 [s / 2]

waarbij de vierkante haken de standaard plafondfunctie aangeven.

Verrassend genoeg verbruiken arrays van de primitieve typen lang en dubbel meer geheugen dan hun wrapper-klassen Lang en Dubbele.

Dat kunnen we ook zien arrays met één element van primitieve typen zijn bijna altijd duurder (behalve lang en dubbel) dan het overeenkomstige referentietype.

3.3. Prestatie

De prestatie van een Java-code is een vrij subtiel probleem, het hangt sterk af van de hardware waarop de code draait, van de compiler die bepaalde optimalisaties zou kunnen uitvoeren, van de toestand van de virtuele machine, van de activiteit van andere processen in de besturingssysteem.

Zoals we al hebben vermeld, leven de primitieve typen in de stapel terwijl de referentietypes in de heap leven. Dit is een dominante factor die bepaalt hoe snel de objecten worden benaderd.

Om te laten zien hoeveel de bewerkingen voor primitieve typen sneller zijn dan die voor wrapper-klassen, laten we een array van vijf miljoen elementen maken waarin alle elementen gelijk zijn, behalve de laatste; dan voeren we een zoekopdracht uit voor dat element:

while (! pivot.equals (elementen [index])) {index ++; }

en vergelijk de prestaties van deze bewerking voor het geval dat de array variabelen van de primitieve typen bevat en voor het geval dat het objecten van de referentietypes bevat.

We gebruiken de bekende JMH-benchmarkingtool (zie onze tutorial over het gebruik ervan), en de resultaten van de opzoekoperatie kunnen in deze grafiek worden samengevat:

Zelfs voor zo'n eenvoudige bewerking kunnen we zien dat er meer tijd nodig is om de bewerking voor wrapper-klassen uit te voeren.

In het geval van meer gecompliceerde bewerkingen zoals optellen, vermenigvuldigen of delen, kan het snelheidsverschil enorm toenemen.

3.4. Standaard waarden

Standaardwaarden van de primitieve typen zijn 0 (in de bijbehorende weergave, d.w.z. 0, 0.0d etc) voor numerieke typen, false voor het booleaanse type, \ u0000 voor het char-type. Voor de wrapper-klassen is de standaardwaarde nul.

Het betekent dat de primitieve typen alleen waarden mogen verwerven uit hun domeinen, terwijl de referentietypes een waarde kunnen krijgen (nul) die in zekere zin niet tot hun domeinen behoren.

Hoewel het niet als een goede gewoonte wordt beschouwd om variabelen niet geïnitialiseerd te laten, kunnen we soms een waarde toekennen nadat deze is gemaakt.

In een dergelijke situatie, als een primitieve typevariabele een waarde heeft die gelijk is aan het standaardtype, moeten we uitzoeken of de variabele echt is geïnitialiseerd.

Er is niet zo'n probleem met variabelen van een wrapper-klasse sinds de nul waarde is een vrij duidelijke indicatie dat de variabele niet is geïnitialiseerd.

4. Gebruik

Zoals we hebben gezien, zijn de primitieve typen veel sneller en hebben ze veel minder geheugen nodig. Daarom willen we ze misschien liever gebruiken.

Aan de andere kant staat de huidige Java-taalspecificatie het gebruik van primitieve typen in de geparametriseerde typen (generieken), in de Java-collecties of de Reflection API niet toe.

Als onze applicatie verzamelingen nodig heeft met een groot aantal elementen, zouden we moeten overwegen om arrays te gebruiken met een zo ‘economisch’ type mogelijk, zoals geïllustreerd op de bovenstaande plot.

5. Conclusie

In deze tutorial hebben we geïllustreerd dat de objecten in Java langzamer zijn en een grotere geheugenimpact hebben dan hun primitieve analogen.

Zoals altijd zijn codefragmenten te vinden in onze repository op GitHub.