Java String Pool
Cómo funciona el String pool de Java, por qué se internan los literales y el método intern().
Un programa Java típico crea miles de strings, y una gran fracción de ellos son los mismos caracteres que algún otro string en otra parte del programa. Nombres de métodos. Claves de configuración. Mensajes de error. Etiquetas de campos. La JVM considera que esta redundancia vale la pena resolverla — mantiene una región especial llamada el string pool (o tabla de internado de strings) y otorga a cada literal que aparece en el código fuente una entrada compartida allí. Dos literales con los mismos caracteres terminan apuntando al mismo objeto.
Ese intercambio tiene consecuencias visibles para la comparación de identidad (==), el uso de memoria y un pequeño número de errores sutiles en torno a intern(). Este capítulo trata sobre las reglas.
Los literales se agrupan en el pool; new String(...) no
El hecho más 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 objectCada literal de string que ve el compilador se agrega al pool la primera vez que se carga. Las apariciones posteriores del mismo literal — en cualquier parte del programa, en cualquier clase — devuelven la misma referencia. Así, a y b son el mismo objeto.
new String("hello") fuerza una nueva asignación en el heap. El argumento "hello" sigue estando en el pool (porque es un literal), pero el constructor lo copia en un objeto completamente nuevo fuera del pool. Por lo tanto, c y a tienen contenido igual pero identidades diferentes.
Esta es la razón por la que "usa equals, no ==" se repite en cada libro de texto de Java. La comparación de identidad funciona para literales simples, pero falla en el momento en que un string proviene de new, de un analizador, de entrada de red o de una concatenación que el compilador no plegó en tiempo de compilación.
Qué vive en el pool
El pool se llena de dos maneras:
- Literales de string en el código fuente. El compilador emite cada literal único como una entrada
CONSTANT_Stringen el pool de constantes de la clase; la JVM lo resuelve en un objetoStringreal en el pool residente en el heap la primera vez que la clase lo utiliza. - Llamadas explícitas a
intern(). CualquierStringdel que tenga una referencia puede agregarse al pool llamando as.intern(). El método devuelve la instancia del pool — que es la misma referencia para cada llamador que interna contenido igual.
Los strings calculados — a + b, s.substring(...), resultados de String.format — no se agrupan automáticamente. Viven donde el GC los colocó y tienen la identidad que sea.
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 instanceEl segundo caso es la trampa. y se calcula a partir de dos literales, pero el compilador pliega la concatenación en tiempo de compilación, por lo que el resultado es simplemente otro literal — agrupado en el pool. z involucra un new en tiempo de ejecución, el compilador no puede plegarlo, y el objeto resultante vive fuera del pool.
El método intern()
String#intern() hace dos cosas en una sola llamada:
- Si ya existe en el pool un string con los mismos caracteres, devuelve esa referencia del pool.
- De lo contrario, agrega este string al pool y lo devuelve.
El segundo comportamiento es el útil cuando se construyen strings en tiempo de ejecución a partir de un vocabulario pequeño pero de alta frecuencia — nombres de encabezados HTTP analizados desde bytes, tokens de un lexer, nombres de columnas leídos de un driver de base de datos. Internarlos colapsa N objetos separados en uno y permite que las comparaciones posteriores usen == si lo has medido como que 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"La trampa: cada llamada a intern() tiene un costo de búsqueda hash, y los strings del pool viven en una tabla hash de tamaño fijo que no se reduce. Si internas entrada ilimitada (consultas de búsqueda escritas por el usuario, IDs de solicitudes), llenas el pool lentamente con strings que nunca se reutilizarán — una fuga de memoria a cámara lenta. Interna solo cuando (a) el conjunto de valores es acotado y (b) has medido un problema que vale la pena resolver.
Aspectos internos del pool (brevemente)
El pool está implementado como una tabla hash dentro de la JVM. En HotSpot es una StringTable con una capacidad predeterminada que se ha ajustado hacia arriba a lo largo de los años (actualmente 65,536 buckets en la mayoría de las compilaciones). Puedes inspeccionarla desde la línea de comandos:
java -XX:+PrintStringTableStatistics MyAppPara el código de aplicación, la implementación es invisible: no puedes preguntar "¿está este string en el pool?" mediante la API pública, y no necesitas hacerlo. El comportamiento visible es == en literales iguales, e intern() para optar por incluir strings calculados.
Por qué == sigue siendo incorrecto para strings
El pool puede hacer que == parezca funcionar con entradas de prueba:
String a = "hello";
String b = "hello";
if (a == b) { ... } // happens to be trueLuego alguien pasa el string por BufferedReader.readLine() y == silenciosamente se vuelve falso. El contrato que quieres es "¿tienen estos los mismos caracteres?", y ese contrato se escribe a.equals(b). El pool es una optimización de memoria, no una estrategia de comparación — nunca confíes en él para la corrección del programa.
Un ejemplo práctico
El ejemplo a continuación hace visible el comportamiento del pool. Cada llamada a printRef muestra el hash de identidad del sistema (un sustituto de una línea para "¿cuál objeto es este?") para que puedas ver dónde los literales comparten almacenamiento y dónde los strings calculados no lo hacen.
Lee primero los hashes de identidad: los literales y el pliegue en tiempo de compilación comparten uno. runtimeConcat y fresh tienen el suyo propio. interned coincide con el literal nuevamente, porque intern() devolvió la instancia del pool, no la asignada con new. Los resultados de == se derivan directamente de las identidades; equals devuelve true para todos ellos porque, en términos de contenido, realmente son iguales.
Qué sigue
El pool existe porque String es inmutable — compartir el mismo objeto entre llamadores solo es seguro si nadie puede cambiar su contenido. El próximo capítulo profundiza en ese hilo: por qué se eligió la inmutabilidad, qué beneficios aporta y el trade-off de diseño que impone. Continúa en Inmutabilidad de String en Java.