Secondo alcuni sondaggi come quello di JetBrains, la versione 8 di Java è al momento quella più utilizzata in assoluto dagli sviluppatori di tutto il mondo, nonostante si tratti di una release del 2014.
Quello che state leggendo è il primo di una serie di articoli intitolata “Superare Java 8”, ispirata ai contenuti del mio ultimo libro “Il nuovo Java”. Questi articoli accompagneranno passo dopo passo il lettore all'esplorazione delle più importanti caratteristiche introdotte a partire dalla versione 9. L'obiettivo è quello di far acquisire la consapevolezza di quanto è importante aggiornare le proprie conoscenze relative a Java, spiegando gli enormi vantaggi che offrono le ultime versioni del linguaggio.
In questo articolo parleremo di compact strings, una meccanismo introdotto con Java 9, che rappresenta uno dei più validi motivi per abbandonare Java 8 ed aggiornarsi ad una delle versioni più recenti
Spoiler Alert
La classe String
, è statisticamente la classe più utilizzata nella
programmazione Java.
Sembra quindi importante porsi il problema di quanto siano efficienti gli oggetti
istanziati da questa classe. La buona notizia è che a partire da Java 9 tali oggetti
sono nettamente più performanti rispetto alle versione precedente. Inoltre questo
vantaggio si ottiene praticamente senza sforzi, ovvero basterà lanciare il nostro
programma con una JVM versione 9 (o superiore), senza adottare nessun accorgimento per
quanto riguarda il nostro codice. Andiamo quindi a capire cosa sono le compact
strings e come utilizzarle.
Dietro le quinte
Figura 1 - File src.zip in JDK 8.
Sino a Java 8, all'interno della classe String
era utilizzato un
array di
char
per
memorizzare i caratteri che componeva la stringa. È possibile verificarlo
leggendo il
codice sorgente della classe String
. Per farlo, basterà cercare il
file
String.java
all'interno del file src.zip
posizionato
nella cartella di
installazione del JDK versione 8 (vedi figura 1 a lato).
Tale file infatti
contiene tutti i file sorgenti della libreria standard di Java.
Quindi, dopo
averlo decompresso, possiamo entrare nella cartella java
e poi
nella cartella lang
dove troveremo il sorgente della classe String.java
(infatti la classe String
appartiene al package
java.lang
).
Se apriamo tale file con un qualsiasi editor, possiamo verificare che la classe String
è dichiarata come segue (abbiamo rimosso alcuni commenti ed altri elementi non utili
alla nostra discussione):
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; //omesso il resto del codice
Sino a Java 8 quindi, l'esistenza dell'array di caratteri value
implicava
che per ogni
carattere di una stringa erano allocati 16 bit (2 byte) di memoria.
In realtà, nella stragrande maggior parte delle applicazioni, vengono utilizzati
caratteri che è possibile immagazzinare in solo 8 bit (1 byte). Quindi per ottenere
maggiori prestazioni in termini di velocità e di utilizzo della memoria nei nostri
programmi, in Java 9 l'implementazione della classe
String
è stata rivista per essere supportata da un array di
byte
invece che da un array di
char
. Segue la parte iniziale della dichiarazione della classe
String
nella versione 15 di Java, privata degli elementi non interessanti:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final byte[] value; /** * The identifier of the encoding used to encode the bytes in * {@code value}. */ private final byte coder; //omesso il resto del codice
Figura 2 - File src.zip in JDK 15.
Dalla versione 9 del JDK il file src.zip
è stato spostato nella
cartella lib
(vedi Figura 2 a lato), e i package sono stati inclusi
nelle cartelle che
rappresentano i moduli. Quindi il sorgente String
.java si trova ora
sotto le cartelle java.base/java/lang
. Infatti,
java.base
è il nome del modulo che contiene il package java.lang
.
È comunque sempre possibile utilizzare caratteri meno comuni che hanno bisogno
di essere immagazzinati in 16 bit (2 byte). Infatti, internamente alla classe
String
, è stato implementato un meccanismo basato sulla variabile
coder
che si
occupa di allocare la giusta quantità di byte per ogni carattere.
Tale meccanismo è noto con il nome di compact strings, e dalla versione 9 di Java è il metodo utilizzato di default dalla JVM. A livello di codice non cambia nulla, utilizzeremo le stringhe come le abbiamo sempre utilizzate. Le applicazioni Java però avranno prestazioni migliori.
Davvero useremo metà della memoria per le stringhe?
Anche se abbiamo notato che oggi la classe String
viene supportata da un
array di
byte
invece che da un array di char
come nella versione 8,
purtroppo in Java non è possibile determinare a priori quanta memoria verrà utilizzata
da un programma. Infatti, essa è gestita automaticamente dai complicati meccanismi del
Garbage Collector, e ad ogni esecuzione il nostro programma potrebbe usare quantità di
memoria molto differenti tra loro. Inoltre, in Java non esiste un metodo per conoscere
precisamente la quantità di memoria utilizzata per un certo oggetto in un dato momento
come è possibile fare con altri linguaggi. Con una strategia basata sull'interfaccia
Instrumentation
del package java.lang.instrument
, è possibile
avere un'approssimazione della dimensione di un oggetto, ma questo non vale per le
stringhe che, essendo oggetti immutabili, vengono allocate in memoria in maniera
differente rispetto agli altri oggetti. Quindi, anche se il meccanismo delle compact
strings sembra implicare un risparmio di memoria, questo non è certo né dimostrabile.
Vediamo allora quale vantaggio comporta l'utilizzo di un JDK versione 9 o superiore con
un esempio di codice.
Esempio pratico
Consideriamo il seguente programma:
public class CompactStringsDemo { public static void main(String[] args) { long tempoIniziale = System.currentTimeMillis(); long limite = 100_000; String s = ""; for (int i = 0; i < limite; i++) { s += limite; } long tempoTotale = System.currentTimeMillis() - tempoIniziale; System.out.println("Create " + limite + " stringhe in " + tempoTotale + " millisecondi"); } }
In questa classe vengono istanziate 100.000 stringhe (che contengono proprio i primi
100.000 numeri) che vengono concatenate. Inoltre sono calcolati e stampati i
millisecondi che occorrono per creare queste istanze e concatenarle.
Proviamo a lanciare l'applicazione 5 volte utilizzando il JDK versione 15.1, ed
analizziamo gli output:
java CompactStringsDemo Create 100000 stringhe in 3539 millisecondi java CompactStringsDemo Create 100000 stringhe in 3548 millisecondi java CompactStringsDemo Create 100000 stringhe in 3564 millisecondi java CompactStringsDemo Create 100000 stringhe in 3561 millisecondi java CompactStringsDemo Create 100000 stringhe in 3609 millisecondi
Possiamo osservare che per ogni lancio la velocità dell'applicazione è quasi costante, e
si attesta intorno a circa 3,5 secondi.
Proviamo allora a disabilitare le compact
strings utilizzando l'opzione -XX:-CompactStrings
, e provare ad eseguire la
stessa applicazione 5 volte per poi analizzare i risultati:
java -XX:-CompactStrings CompactStringsDemo Create 100000 stringhe in 8731 millisecondi java -XX:-CompactStrings CompactStringsDemo Create 100000 stringhe in 8263 millisecondi java -XX:-CompactStrings CompactStringsDemo Create 100000 stringhe in 8547 millisecondi java -XX:-CompactStrings CompactStringsDemo Create 100000 stringhe in 8602 millisecondi java -XX:-CompactStrings CompactStringsDemo Create 100000 stringhe in 8353 millisecondi
Anche in questo caso, le performance in termini di velocità sono quasi costanti, ma
molto peggiori rispetto a quando abbiamo usato le compact strings. Infatti, la velocità
media di esecuzione di questa applicazione senza compact strings risulta essere di circa
8,5 secondi, mentre quando abbiamo utilizzato le compact strings, la media era solo di
circa 3,5 secondi. Un vantaggio notevole che ci ha fatto risparmiare quasi il 60% del
tempo.
Se addirittura ricompiliamo e rilanciamo il programma direttamente con l'ultima build
di Java 8 (JDK 1.8.0_261), i vantaggi risultano ancora più evidenti:
"C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo Create 100000 stringhe in 31113 millisecondi "C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo Create 100000 stringhe in 30376 millisecondi "C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo Create 100000 stringhe in 32868 millisecondi "C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo Create 100000 stringhe in 32508 millisecondi "C:\Program Files\Java\jdk1.8.0_261\bin\java" CompactStringsDemo Create 100000 stringhe in 35328 millisecondi
Il peggioramento delle prestazioni questa volta è davvero notevole: con un JDK 15 e
le compact strings le prestazioni dell'applicazione erano migliori di quasi 10 volte!
Ovviamente, questo non significa che tutti i programmi avranno dei miglioramenti così
eccezionali, perché il nostro esempio era esclusivamente basato sull'allocazione e la
concatenazione di stringhe.
Per quanto riguarda il risparmio dell'utilizzo della memoria, benché probabile, come
abbiamo detto non è dimostrabile visto che è il Garbage Collector svolge un lavoro
complesso che si basa sulla situazione del momento.
Conclusioni
In questo articolo abbiamo visto il primo valido motivo per superare Java 8. Le compact
strings introdotte a partire dalla versione 9, permettono ai nostri programmi di essere
più performanti quando vengono utilizzate le stringhe. Essendo la classe
String
statisticamente la classe più utilizzata nei programmi Java, possiamo concludere che il
solo utilizzare un JDK con versione maggiore di 8 garantirà una velocità di esecuzione
maggiore alle nostre applicazioni. Abbiamo anche potuto constatare che un JDK 15 senza
utilizzare le compact strings garantisce comunque prestazioni notevolmente superiori
rispetto all'ultima build del JDK 8.
Aggiornare il JDK sembra proprio il primo passo da fare.