W3docs

Immutabilità delle String in Java

Perché la classe String di Java è immutabile: implicazioni per sicurezza, caching, hashing e thread safety.

Una String in Java non può essere modificata dopo la sua creazione. Una volta che "hello" esiste, nessun metodo, nessun trucco di reflection, nessuna assegnazione ingegnosa può riscrivere i caratteri di quell'oggetto particolare. Ogni operazione che "modifica" una stringa restituisce in realtà una nuova String. La classe impone questo: il campo che contiene i byte è private final, la classe stessa è final, e non esiste setter pubblico, né append, né clear.

Quella scelta — l'immutabilità — non è una preferenza stilistica. È la decisione portante che rende sicuro il pool di stringhe, affidabile l'hashing, libera la condivisione multi-thread e rende possibili alcune sottili garanzie di sicurezza.

Cosa significa davvero "immutabile"

String s = "hello";
s.toUpperCase();           // returns "HELLO" — the return value is dropped
System.out.println(s);     // prints "hello"

s = s.toUpperCase();       // s now *points at* a different String
System.out.println(s);     // prints "HELLO"

La variabile s può essere riassegnata — questa è una proprietà della variabile, non dell'oggetto. L'oggetto originariamente creato con "hello" è immutato ovunque, per sempre, indipendentemente da ciò a cui s punta in seguito. Se un'altra variabile fa ancora riferimento a esso, quella variabile vede ancora "hello".

String a = "hello";
String b = a;
a = a.toUpperCase();
System.out.println(a);     // "HELLO"
System.out.println(b);     // "hello" — still the original

Questo è ciò che le persone intendono quando dicono che le stringhe sono simili a valori: il contenuto di un riferimento String è stabile quanto il contenuto di un int.

Perché i progettisti della JVM hanno scelto l'immutabilità

Dall'immutabilità derivano alcune proprietà, ognuna delle quali vale una reale performance o una reale sicurezza.

Il pool di stringhe è sicuro. Se "hello" potesse essere modificata in-place, condividere un'istanza memorizzata nel pool attraverso l'intero programma sarebbe un disastro: modificarla in un punto la cambierebbe silenziosamente ovunque. L'immutabilità è ciò che rende possibile il pool di stringhe.

hashCode() può essere memorizzato nella cache. String calcola il suo hash alla prima chiamata e lo conserva in un campo privato. Quel valore memorizzato nella cache sarebbe una bugia se i caratteri potessero cambiare in seguito, corrompendo ogni HashMap<String, ?> con chiave su quella stringa. Poiché il contenuto è stabile, la cache è permanente.

Le letture concorrenti non richiedono sincronizzazione. Due thread che leggono lo stesso riferimento String non potranno mai osservare un valore parzialmente modificato. Non c'è synchronized, né volatile, né danza con le memory-barrier — non c'è nulla che potrebbe cambiare. Confrontalo con un buffer mutabile, dove dovresti copiare, bloccare o limitare la proprietà.

Il caricamento delle classi, la reflection e i controlli di sicurezza possono fidarsi degli argomenti stringa. Un ClassLoader risolve i nomi delle classi dalle String passate dal chiamante. Se la stringa potesse essere modificata da un altro thread tra il controllo di sicurezza e l'apertura del file, si avrebbe una vulnerabilità di race-condition — il classico bug time-of-check / time-of-use. Con le stringhe immutabili, il valore validato è identico al valore utilizzato.

Gli argomenti dei metodi non richiedono copie difensive. Quando passi una String a un metodo, non ti preoccupi che venga mutata e ti sorprenda al ritorno. Il ricevente può memorizzare direttamente il riferimento; anche il chiamante può continuare a usare il suo riferimento.

Il costo: la mutazione massiva è costosa

C'è un prezzo. Costruire una stringa di 10.000 caratteri un carattere alla volta con += alloca una nuova String ad ogni passo, copiando ogni carattere già presente più il nuovo. Si tratta di lavoro quadratico — O(n²) per un'attività che dovrebbe essere O(n).

// Don't do this for large n
String s = "";
for (int i = 0; i < n; i++) {
  s += i + ",";
}

La risposta della libreria standard sono i buffer mutabili — StringBuilder per il codice single-thread e StringBuffer per il raro caso condiviso. Mantengono un array ridimensionabile, accodano in O(1) ammortizzato e producono una singola String immutabile alla fine con toString(). Questo è il pattern canonico per assemblare stringhe.

StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
  sb.append(i).append(',');
}
String s = sb.toString();

I JDK moderni ottimizzano le catene + brevi e a forma statica tramite StringConcatFactory, quindi "hello, " + name + "!" va bene. Il caso da evitare è += dentro un ciclo su un conteggio sconosciuto.

Tentare di violare l'immutabilità

La reflection può tecnicamente raggiungere il campo privato value e sostituirlo. Farlo è un comportamento indefinito per quanto riguarda la JVM: il JIT assume che le stringhe siano immutabili e incorporerà il hashCode memorizzato nella cache, condividerà i riferimenti attraverso il pool e salterà le read barrier basandosi su quella promessa. Mutare una String tramite reflection può corrompere silenziosamente codice non correlato che mantiene un riferimento allo stesso oggetto. Non farlo. Se hai bisogno di mutabilità, hai StringBuilder per questo.

Implicazioni per la sicurezza

Due casi concreti in cui l'immutabilità è importante per la sicurezza:

  • Percorsi di file e nomi di classi. Passati ad API che eseguono un controllo di accesso prima di aprire o caricare. Se un percorso potesse cambiare tra controllo e utilizzo, le sandbox sarebbero aggirabili.
  • Chiavi di ClassLoader e chiavi di mappe String. Gli hash code stabili significano che un attaccante non può costruire una chiave che "si adatta" in un punto e si sposta silenziosamente in un altro.

Il rovescio della medaglia: memorizzare le password in una String è una pratica sbagliata per il motivo opposto. Una volta che una password risiede in una String, non puoi azzerarla — i byte rimangono nella memoria heap finché il GC non li recupera, possibilmente dopo che è stato scritto un heap dump. Per le password, usa char[] (che puoi riempire manualmente con zeri) o — meglio — javax.crypto.SecretKey e simili. Il Console.readPassword() del JDK restituisce char[] proprio per questo motivo.

Un esempio pratico

Questo programma crea una stringa, la passa a diversi chiamanti, fa "mutare" ognuno di loro e stampa ciò che ogni variabile vede in seguito. L'oggetto originale è visitato da quattro riferimenti e sopravvive invariato. Il singolo buffer mutabile alla fine è l'alternativa canonica quando hai davvero bisogno di costruire una stringa.

java— editable, runs on the server

Osserva i due confronti con ==. original e alias sono letteralmente lo stesso oggetto, quindi l'identità vale. original e upper hanno contenuti correlati ma upper è un oggetto nuovo — non è possibile che upperCase abbia modificato quello che gli è stato passato. Questa è la garanzia su cui ogni sviluppatore Java fa affidamento senza nemmeno pensarci.

Cosa c'è dopo

Quando hai davvero bisogno di una stringa che puoi modificare, la libreria standard ha un cugino mutabile di String. È il motore dietro ogni catena + che il compilatore ottimizza, e la risposta giusta ogni volta che altrimenti useresti += in un ciclo. Continua con Java StringBuilder.

Esercitati

Pratica
Quale delle seguenti **non** è un vantaggio dell'immutabilità di `String` in Java?
Quale delle seguenti **non** è un vantaggio dell'immutabilità di `String` in Java?
Was this page helpful?