W3docs

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 object

Ogni 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:

  1. Letterali stringa nel codice sorgente. Il compilatore emette ogni letterale unico come voce CONSTANT_String nel constant pool della classe; la JVM lo risolve in un vero oggetto String nel pool residente nell'heap la prima volta che la classe lo utilizza.
  2. Chiamate esplicite a intern(). Qualsiasi String di cui si ha un riferimento può essere aggiunta al pool chiamando s.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.formatnon 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 instance

Il 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 MyApp

Per 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 true

Poi 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.

java— editable, runs on the server

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.

Esercitazione

Pratica
Due variabili `String` `a` e `b` sono state entrambe assegnate con lo stesso letterale 'hi' da qualche parte nel programma. Una terza, `c`, è stata creata con `new String('hi')`. Quale risultato è garantito?
Due variabili `String` `a` e `b` sono state entrambe assegnate con lo stesso letterale 'hi' da qualche parte nel programma. Una terza, `c`, è stata creata con `new String('hi')`. Quale risultato è garantito?
Was this page helpful?