Java Primitives versus Objects

1. Oversigt

I denne vejledning viser vi fordele og ulemper ved at bruge primitive Java-typer og deres indpakket kolleger.

2. Java Type System

Java har et dobbelt system, der består af primitive som int, boolsk og referencetyper som f.eks Heltal,Boolsk. Hver primitiv type svarer til en referencetype.

Hvert objekt indeholder en enkelt værdi af den tilsvarende primitive type. Det indpakningsklasser er uforanderlige (så deres tilstand ikke kan ændres, når objektet er konstrueret) og er endeligt (så vi ikke kan arve fra dem).

Under emhætten udfører Java en konvertering mellem de primitive og referencetyperne, hvis en faktisk type er forskellig fra den erklærede:

Heltal j = 1; // autoboxing int i = nyt heltal (1); // unboxing 

Processen med at konvertere en primitiv type til en reference kaldes autoboxing, den modsatte proces kaldes unboxing.

3. Fordele og ulemper

Beslutningen, hvilket objekt der skal bruges, er baseret på, hvilken applikationsydelse vi forsøger at opnå, hvor meget ledig hukommelse vi har, mængden af ​​tilgængelig hukommelse og hvilke standardværdier, vi skal håndtere.

Hvis vi ikke står over for nogen af ​​disse, kan vi ignorere disse overvejelser, selvom det er værd at kende dem.

3.1. Enkelt element hukommelses fodaftryk

Bare til reference har de primitive type variabler følgende indvirkning på hukommelsen:

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

I praksis kan disse værdier variere afhængigt af implementeringen af ​​Virtual Machine. I Oracles VM kortlægges den boolske type for eksempel til int-værdierne 0 og 1, så det tager 32 bits, som beskrevet her: Primitive typer og værdier.

Variabler af disse typer lever i stakken og fås derfor hurtigt. For detaljer anbefaler vi vores tutorial om Java-hukommelsesmodellen.

Referencetyperne er objekter, de lever på dyngen og er relativt langsomme at få adgang til. De har en vis overhead angående deres primitive kolleger.

De konkrete værdier for overhead er generelt JVM-specifikke. Her præsenterer vi resultater for en 64-bit virtuel maskine med disse parametre:

java 10.0.1 2018-04-17 Java (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, blandet tilstand)

For at få et objekts interne struktur kan vi bruge Java Object Layout-værktøjet (se vores anden vejledning om, hvordan man får størrelsen på et objekt).

Det viser sig, at en enkelt forekomst af en referencetype på denne JVM optager 128 bits undtagen Lang og Dobbelt der optager 192 bits:

  • Boolsk - 128 bits
  • Byte - 128 bits
  • Kort, karakter - 128 bits
  • Heltal, flyde - 128 bits
  • Lang, dobbelt - 192 bits

Vi kan se, at en enkelt variabel på Boolsk type optager så meget plads som 128 primitive, mens en Heltal variabel optager så meget plads som fire int dem.

3.2. Memory Footprint for Arrays

Situationen bliver mere interessant, hvis vi sammenligner, hvor meget hukommelse, der optager arrays af de pågældende typer.

Når vi opretter arrays med det forskellige antal elementer for hver type, får vi et plot:

der viser, at typerne er grupperet i fire familier med hensyn til, hvordan hukommelsen Frk) afhænger af antallet af elementer s i arrayet:

  • lang, dobbelt: m (s) = 128 + 64 s
  • kort, char: m (s) = 128 + 64 [s / 4]
  • byte, boolsk: m (s) = 128 + 64 [s / 8]
  • resten: m (s) = 128 + 64 [s / 2]

hvor firkantede parenteser betegner standardloftfunktionen.

Overraskende nok bruger arrays af de primitive typer lang og dobbelt mere hukommelse end deres indpakningsklasser Lang og Dobbelt.

Det kan vi enten se enkeltelementarrays af primitive typer er næsten altid dyrere (undtagen lang og dobbelt) end den tilsvarende referencetype.

3.3. Ydeevne

Udførelsen af ​​en Java-kode er et ganske subtilt problem, det afhænger meget af den hardware, som koden kører på, af compileren, der muligvis udfører visse optimeringer, af tilstanden til den virtuelle maskine, af aktiviteten af ​​andre processer i operativ system.

Som vi allerede har nævnt, lever de primitive typer i stakken, mens referencetyperne lever i bunken. Dette er en dominerende faktor, der bestemmer, hvor hurtigt genstande får adgang.

For at demonstrere, hvor meget operationerne for primitive typer er hurtigere end dem for wrapper-klasser, lad os oprette et fem millioner elementarray, hvor alle elementer er ens bortset fra den sidste; så udfører vi et opslag efter dette element:

mens (! pivot.equals (elementer [index])) {index ++; }

og sammenlign udførelsen af ​​denne operation for det tilfælde, hvor arrayet indeholder variabler af de primitive typer, og for case, når det indeholder objekter af referencetyperne.

Vi bruger det velkendte JMH benchmarking-værktøj (se vores vejledning om, hvordan du bruger det), og resultaterne af opslagsoperationen kan opsummeres i dette diagram:

Selv for en så enkel operation kan vi se, at det kræves mere tid til at udføre operationen for indpakningsklasser.

I tilfælde af mere komplicerede operationer som summering, multiplikation eller division kan forskellen i hastighed skyrocket.

3.4. Standardværdier

Standardværdier for de primitive typer er 0 (i den tilsvarende repræsentation, dvs. 0, 0,0d osv.) til numeriske typer, falsk til den boolske type, \ u0000 for char-typen. For indpakningsklasser er standardværdien nul.

Det betyder, at de primitive typer kun kan erhverve værdier fra deres domæner, mens referencetyperne muligvis erhverver en værdi (nul) at det i en eller anden forstand ikke hører til deres domæner.

Selvom det ikke betragtes som en god praksis at lade variabler være uinitialiseret, kan vi nogle gange tildele en værdi efter oprettelsen.

I en sådan situation, når en primitiv type variabel har en værdi, der er lig med dens standard standard, skal vi finde ud af, om variablen virkelig er initialiseret.

Der er ikke et sådant problem med en wrapper klasse variabler siden nul værdi er en tydelig indikation på, at variablen ikke er initialiseret.

4. Anvendelse

Som vi har set, er de primitive typer meget hurtigere og kræver meget mindre hukommelse. Derfor vil vi måske foretrække at bruge dem.

På den anden side tillader den nuværende Java-sprogspecifikation ikke anvendelse af primitive typer i de parametriserede typer (generiske), i Java-samlinger eller Reflection API.

Når vores applikation har brug for samlinger med et stort antal elementer, bør vi overveje at bruge arrays med en så "økonomisk" type som muligt, som det er illustreret på plottet ovenfor.

5. Konklusion

I denne tutorial illustrerede vi, at objekterne i Java er langsommere og har større hukommelsespåvirkning end deres primitive analoger.

Som altid kan kodeuddrag findes i vores lager på GitHub.