Java String Pool
Come funziona il Java String pool, perché i letterali vengono internati e il metodo intern().
Un tipico programma Java crea migliaia di stringhe, e una grande parte di esse sono gli stessi caratteri di qualche altra stringa altrove nel programma. Nomi di metodi. Chiavi di configurazione. Messaggi di errore. Etichette di campo. La JVM considera questa ridondanza un problema che vale la pena risolvere — mantiene una regione speciale chiamata string pool (o string intern table) e assegna a ogni letterale che appare nel codice sorgente una voce condivisa lì. Due letterali con gli stessi caratteri finiscono per puntare allo stesso oggetto.
Questa condivisione ha conseguenze visibili per il confronto di identità (==), l'uso della memoria e un piccolo numero di bug sottili attorno a intern(). Questo capitolo riguarda le regole.
I letterali sono memorizzati nel pool, new String(...) no
Il fatto singolarmente più importante:
String a = "hello";
String b = "hello";
System.out.println(a == b); // true — same pooled object
String c = new String("hello");
System.out.println(a == c); // false — new String, fresh objectOgni letterale stringa che il compilatore vede viene aggiunto al pool la prima volta che viene caricato. Le occorrenze successive dello stesso letterale — ovunque nel programma, in qualsiasi classe — restituiscono lo stesso riferimento. Quindi a e b sono lo stesso oggetto.
new String("hello") forza una nuova allocazione nell'heap. L'argomento "hello" è ancora nel pool (perché è un letterale), ma il costruttore lo copia in un oggetto completamente nuovo al di fuori del pool. c e a hanno quindi contenuti uguali ma identità diverse.
Questo è il motivo per cui "usare equals, non ==" viene martellato in ogni libro di testo Java. Il confronto di identità funziona per i letterali semplici, ma si rompe nel momento in cui una stringa proviene da new, da un parser, da input di rete, o da una concatenazione che il compilatore non ha piegato in fase di compilazione.
Cosa vive nel pool
Il pool viene popolato in due modi:
- Letterali stringa nel codice sorgente. Il compilatore emette ogni letterale unico come voce
CONSTANT_Stringnel constant pool della classe; la JVM lo risolve in un vero oggettoStringnel pool residente nell'heap la prima volta che la classe lo utilizza. - Chiamate esplicite a
intern(). QualsiasiStringdi cui si ha un riferimento può essere aggiunta al pool chiamandos.intern(). Il metodo restituisce l'istanza nel pool — che è lo stesso riferimento per ogni chiamante che interna contenuti uguali.
Le stringhe calcolate — a + b, s.substring(...), risultati di String.format — non vengono aggiunte automaticamente al pool. Vivono ovunque il GC le abbia collocate e hanno qualsiasi identità capiti loro di avere.
String x = "java";
String y = "ja" + "va"; // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y); // true
System.out.println(x == z); // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instanceIl secondo caso è la trappola. y è calcolato da due letterali, ma il compilatore piega la concatenazione in fase di compilazione, quindi il risultato è solo un altro letterale — nel pool. z coinvolge un new a runtime, il compilatore non può piegarlo, e l'oggetto risultante vive fuori dal pool.
Il metodo intern()
String#intern() fa due cose in una sola chiamata:
- Se una stringa con gli stessi caratteri è già nel pool, restituisce quel riferimento nel pool.
- Altrimenti, aggiunge questa stringa al pool e la restituisce.
Il secondo comportamento è quello utile quando si costruiscono stringhe a runtime da un vocabolario piccolo ma ad alta frequenza — nomi di intestazioni HTTP analizzati da byte, token da un lexer, nomi di colonne letti da un driver di database. L'internamento di queste stringhe collassa N oggetti separati in uno e significa che i confronti a valle possono usare == se hai misurato che ne vale la pena.
String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2); // true — both refer to the pooled "status"Il rovescio della medaglia: ogni chiamata a intern() costa una ricerca hash, e le stringhe nel pool vivono in una tabella hash di dimensioni fisse che non si restringe. Se si internano input illimitati (query di ricerca digitate dall'utente, ID di richiesta), si riempie lentamente il pool con stringhe che non verranno mai riutilizzate — una perdita di memoria al rallentatore. Interna solo quando (a) l'insieme dei valori è limitato e (b) hai misurato un problema che vale la pena risolvere.
Interni del pool (brevemente)
Il pool è implementato come una tabella hash all'interno della JVM. Su HotSpot è una StringTable con una capacità predefinita che è stata aumentata nel corso degli anni (attualmente 65.536 bucket nella maggior parte delle build). Puoi ispezionarla dalla riga di comando:
java -XX:+PrintStringTableStatistics MyAppPer il codice applicativo, l'implementazione è invisibile: non puoi chiedere "questa stringa è nel pool?" tramite l'API pubblica, e non ne hai bisogno. Il comportamento visibile è == su letterali uguali, e intern() per inserire le stringhe calcolate nel pool.
Perché == è ancora sbagliato per le stringhe
Il pool può far sembrare che == funzioni sugli input di test:
String a = "hello";
String b = "hello";
if (a == b) { ... } // happens to be truePoi qualcuno passa la stringa attraverso BufferedReader.readLine() e == diventa silenziosamente falso. Il contratto che si vuole è "questi hanno gli stessi caratteri?", e quel contratto si scrive a.equals(b). Il pool è un'ottimizzazione della memoria, non una strategia di confronto — non fare mai affidamento su di esso per la correttezza.
Un esempio pratico
L'esempio seguente rende visibile il comportamento del pool. Ogni chiamata a printRef mostra l'hash di identità del sistema (un sostituto a una riga per "quale oggetto è questo?") in modo da poter vedere dove i letterali condividono la memorizzazione e dove le stringhe calcolate non lo fanno.
Leggi prima gli hash di identità: i letterali e la piegatura in fase di compilazione condividono uno. runtimeConcat e fresh hanno ciascuno il proprio. interned corrisponde di nuovo al letterale, perché intern() ha restituito l'istanza nel pool, non quella allocata con new. I risultati di == seguono direttamente dalle identità; equals restituisce true per tutti loro perché, dal punto di vista del contenuto, sono davvero uguali.
Cosa c'è dopo
Il pool esiste perché String è immutabile — condividere lo stesso oggetto tra i chiamanti è sicuro solo se nessuno può modificarne il contenuto. Il prossimo capitolo tira quel filo: perché è stata scelta l'immutabilità, cosa porta con sé e il compromesso di design che impone. Continua con Immutabilità delle stringhe Java.