Panoramica
In questo tutorial, mostriamo i pro ei contro dell’utilizzo di tipi primitivi Java e le loro controparti avvolte.
Sistema di tipo Java
Java ha un sistema di tipo duplice costituito da primitive come int, boolean e tipi di riferimento come Integer, Boolean. Ogni tipo primitivo corrisponde a un tipo di riferimento.
Ogni oggetto contiene un singolo valore del tipo primitivo corrispondente. Le classi wrapper sono immutabili (in modo che il loro stato non possa cambiare una volta che l’oggetto è stato costruito) e sono definitive (in modo che non possiamo ereditare da loro).
Sotto il cofano, Java esegue una conversione tra i primitivi e tipi di riferimento, se un tipo effettivo è diverso da quello dichiarato:
Integer j = 1; // autoboxingint i = new Integer(1); // unboxing
Il processo di conversione di un tipo primitivo di riferimento è chiamato autoboxing, il processo opposto è chiamato unboxing.
Pro e contro
La decisione su quale oggetto deve essere utilizzato si basa su quali prestazioni dell’applicazione cerchiamo di ottenere, quanta memoria disponibile abbiamo, la quantità di memoria disponibile e quali valori predefiniti dovremmo gestire.
Se non affrontiamo nessuno di questi, possiamo ignorare queste considerazioni anche se vale la pena conoscerle.
3.1. Impronta di memoria singolo elemento
Solo per riferimento, le variabili di tipo primitivo hanno il seguente impatto sulla memoria:
- boolean – 1 bit
- byte – 8 bit
- short, char – 16 bit
- int, float – 32 bit
- long, double – 64 bit
In pratica, questi valori possono variare a seconda dell’implementazione della Macchina Virtuale. Nella VM di Oracle, il tipo booleano, ad esempio, viene mappato ai valori int 0 e 1, quindi richiede 32 bit, come descritto qui: Tipi e valori primitivi.
Le variabili di questi tipi vivono nello stack e quindi sono accessibili velocemente. Per i dettagli, si consiglia il nostro tutorial sul modello di memoria Java.
I tipi di riferimento sono oggetti, vivono sull’heap e sono relativamente lenti da accedere. Hanno un certo sovraccarico riguardo alle loro controparti primitive.
I valori concreti del sovraccarico sono in generale specifici per JVM. Qui, presentiamo i risultati per una macchina virtuale a 64 bit con questi 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)
Per ottenere la struttura interna di un oggetto, possiamo usare lo strumento Java Object Layout (vedere il nostro altro tutorial su come ottenere la dimensione di un oggetto).
Si scopre che una singola istanza di un tipo di riferimento su questo JVM occupa 128 bit, tranne per il Lungo e Doppio che occupano 192 bit:
- Boolean – 128 bit
- Byte 128 bit
- Breve, di Carattere 128 bit
- Integer, Float – 128 bit
- Lungo, Fare doppio 192 bit
Possiamo vedere che una singola variabile di tipo Boolean occupa più spazio 128 primitiva, mentre una variabile di tipo Integer occupa più spazio di quattro int quelli.
3.2. Impronta di memoria per array
La situazione diventa più interessante se confrontiamo la quantità di memoria che occupa gli array dei tipi in esame.
Quando si crea un array con i vari numero di elementi per ogni tipo, si ottiene la trama:
che dimostra che i tipi sono raggruppati in quattro famiglie rispetto a come la memoria m(s) dipende dal numero di elementi di s dell’array:
- long, double: m(s) = 128 + 64 s
- breve, char: m(s) = 128 + 64
- byte, boolean: m(s) = 128 + 64
- il resto: m (s) = 128 + 64
dove le parentesi quadre indicano la funzione standard del soffitto.
Sorprendentemente, gli array dei tipi primitivi long e double consumano più memoria delle loro classi wrapper Long e Double.
Possiamo vedere che gli array a singolo elemento di tipi primitivi sono quasi sempre più costosi (ad eccezione di long e double) rispetto al tipo di riferimento corrispondente.
3.3. Prestazioni
Le prestazioni di un codice Java sono un problema piuttosto sottile, dipende molto dall’hardware su cui viene eseguito il codice, dal compilatore che potrebbe eseguire determinate ottimizzazioni, dallo stato della macchina virtuale, dall’attività di altri processi nel sistema operativo.
Come abbiamo già detto, i tipi primitivi vivono nello stack mentre i tipi di riferimento vivono nell’heap. Questo è un fattore dominante che determina la velocità di accesso agli oggetti.
A dimostrazione di quanto le operazioni per i tipi primitivi sono più veloci rispetto a quelle per le classi wrapper, proviamo a creare un cinque milioni di elemento di matrice in cui tutti gli elementi sono uguali tranne che per l’ultimo; e poi eseguire una ricerca per l’elemento:
while (!pivot.equals(elements)) { index++;}
e confrontare le prestazioni di questa operazione per il caso in cui l’array contiene le variabili di tipo primitivo e per il caso in cui contiene oggetti di tipi di riferimento.
utilizzare il noto JMH strumento di benchmarking (vedi il nostro tutorial su come usarlo), e i risultati dell’operazione di ricerca possono essere riassunti in questa tabella:
Anche per una semplice operazione, si può vedere che è necessario più tempo per eseguire l’operazione per le classi wrapper.
In caso di operazioni più complicate come sommatoria, moltiplicazione o divisione, la differenza di velocità potrebbe salire alle stelle.
3.4. Valori predefiniti
I valori predefiniti dei tipi primitivi sono 0 (nella rappresentazione corrispondente, cioè 0, 0.0d etc) per i tipi numerici, false per il tipo booleano, \u0000 per il tipo char. Per le classi wrapper, il valore predefinito è null.
Significa che i tipi primitivi possono acquisire valori solo dai loro domini, mentre i tipi di riferimento potrebbero acquisire un valore (null) che in un certo senso non appartiene ai loro domini.
Sebbene non sia considerato una buona pratica lasciare le variabili non inizializzate, a volte potremmo assegnare un valore dopo la sua creazione.
In una situazione del genere, quando una variabile di tipo primitivo ha un valore uguale al suo tipo predefinito, dovremmo scoprire se la variabile è stata realmente inizializzata.
Non esiste un problema con le variabili di classe wrapper poiché il valore null è un’indicazione abbastanza evidente che la variabile non è stata inizializzata.
Usage
Come abbiamo visto, i tipi primitivi sono molto più veloci e richiedono molta meno memoria. Pertanto, potremmo preferire il loro utilizzo.
D’altra parte, le attuali specifiche del linguaggio Java non consentono l’utilizzo di tipi primitivi nei tipi parametrizzati (generici), nelle raccolte Java o nell’API Reflection.
Quando la nostra applicazione ha bisogno di raccolte con un gran numero di elementi, dovremmo considerare l’utilizzo di array con il tipo più “economico” possibile, come illustrato nella trama sopra.
Conclusione
Questo tutorial, abbiamo illustrato che gli oggetti in Java sono più lenti e hanno un impatto di memoria maggiore rispetto ai loro analoghi primitivi.
Come sempre, i frammenti di codice possono essere trovati nel nostro repository su GitHub.
Inizia con Spring 5 e Spring Boot 2, attraverso il corso Learn Spring:
>>DAI UN’OCCHIATA AL CORSO