W3docs

Inmutabilidad de String en Java

Por qué la clase String de Java es inmutable: implicaciones para seguridad, caché, hashing y seguridad en hilos.

Un String en Java no puede modificarse después de ser creado. Una vez que "hello" existe, ningún método, ningún truco de reflexión ni ninguna asignación ingeniosa puede reescribir los caracteres de ese objeto en particular. Cada operación que "modifica" un string en realidad devuelve un nuevo String. La clase lo garantiza: el campo que almacena los bytes es private final, la clase en sí misma es final, y no existe ningún setter público, ningún append, ningún clear.

Esa decisión — la inmutabilidad — no es una preferencia estilística. Es la decisión fundamental que hace que el string pool sea seguro, que el hashing sea fiable, que el uso compartido en múltiples hilos sea gratuito y que un puñado de sutiles garantías de seguridad sea posible en absoluto.

Qué significa realmente "inmutable"

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 variable s puede reasignarse — esa es una propiedad de la variable, no del objeto. El objeto creado originalmente con "hello" permanece inalterado en cualquier lugar, para siempre, independientemente de a qué apunte s después. Si otra variable aún lo referencia, esa variable seguirá viendo "hello".

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

Esto es lo que quieren decir las personas cuando afirman que los strings son similares a valores: el contenido de una referencia String es tan estable como el contenido de un int.

Por qué los diseñadores de la JVM eligieron la inmutabilidad

De la inmutabilidad se derivan varias propiedades, y cada una representa un beneficio real de rendimiento o de seguridad.

El string pool es seguro. Si "hello" pudiera modificarse en el lugar, compartir una instancia del pool en todo el programa sería desastroso: modificarlo en un lugar lo cambiaría silenciosamente en todos los demás. La inmutabilidad es lo que hace posible el string pool en absoluto.

hashCode() puede almacenarse en caché. String calcula su hash en la primera llamada y lo guarda en un campo privado. Ese valor en caché sería una mentira si los caracteres pudieran cambiar posteriormente, lo que rompería cualquier HashMap<String, ?> cuya clave sea ese string. Dado que el contenido es estable, la caché es permanente.

Las lecturas concurrentes no necesitan sincronización. Dos hilos que lean la misma referencia String nunca podrán observar un valor modificado a medias. No hay synchronized, ni volatile, ni danza de barreras de memoria — no hay nada que pueda cambiar. Compara esto con un búfer mutable, donde tendrías que copiar, bloquear o restringir la propiedad.

La carga de clases, la reflexión y las comprobaciones de seguridad pueden confiar en los argumentos de tipo string. Un ClassLoader resuelve nombres de clases a partir de Strings pasados por el llamante. Si el string pudiera ser modificado por otro hilo entre la comprobación de seguridad y la apertura del archivo, habría una vulnerabilidad de condición de carrera — el clásico bug de tiempo de comprobación / tiempo de uso. Con strings inmutables, el valor validado es idéntico al valor utilizado.

Los argumentos de método no necesitan copias defensivas. Cuando pasas un String a un método, no te preocupa que sea mutado y te sorprenda al retorno. El receptor puede almacenar la referencia directamente; el llamante también puede seguir usando su referencia.

El coste: la mutación masiva es costosa

Hay un precio. Construir un string de 10.000 caracteres carácter a carácter con += asigna un nuevo String en cada paso, copiando todos los caracteres que ya tiene más el nuevo. Eso es trabajo cuadrático — O(n²) para una tarea de O(n).

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

La respuesta de la biblioteca estándar son los búferes mutables — StringBuilder para código de un solo hilo y StringBuffer para el caso poco frecuente de uso compartido. Mantienen un array redimensionable, añaden en O(1) amortizado y producen un único String inmutable al final con toString(). Ese es el patrón canónico para ensamblar strings.

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

Los JDKs modernos optimizan las cadenas cortas + de forma estática a través de StringConcatFactory, por lo que "hello, " + name + "!" está bien. El caso que hay que evitar es += dentro de un bucle sobre un número desconocido de iteraciones.

Intentar romperlo

La reflexión puede técnicamente llegar al campo privado value y reemplazarlo. Hacerlo es comportamiento indefinido desde el punto de vista de la JVM: el JIT asume que los strings son inmutables e insertará en línea el hashCode cacheado, compartirá referencias a través del pool y omitirá barreras de lectura basándose en esa promesa. Mutar un String mediante reflexión puede corromper silenciosamente código no relacionado que tenga una referencia al mismo objeto. No lo hagas. Si necesitas mutabilidad, tienes StringBuilder para eso.

Implicaciones de seguridad

Dos casos concretos donde la inmutabilidad importa para la seguridad:

  • Rutas de archivo y nombres de clases. Se pasan a APIs que realizan una comprobación de acceso antes de abrir o cargar. Si una ruta pudiera cambiar entre la comprobación y el uso, los sandboxes serían eludibles.
  • Claves de ClassLoader y claves de mapa String. Los códigos hash estables significan que un atacante no puede diseñar una clave que "encaje" en un lugar y se reubique silenciosamente en otro.

La otra cara de la moneda: almacenar contraseñas en un String es una mala práctica por la razón opuesta. Una vez que una contraseña está en un String, no puedes borrarla — los bytes permanecen en la memoria del heap hasta que el GC los reclame, posiblemente después de que se haya escrito un volcado del heap. Para contraseñas, usa char[] (que puedes rellenar manualmente con ceros) o — mejor — javax.crypto.SecretKey y similares. El Console.readPassword() del JDK devuelve char[] precisamente por esta razón.

Un ejemplo completo

Este programa crea un string, lo pasa a varios llamantes, hace que cada uno lo "mute" e imprime lo que ve cada variable después. El objeto original es visitado por cuatro referencias y sobrevive sin cambios. El único búfer mutable al final es la alternativa canónica cuando genuinamente necesitas construir un string de forma incremental.

java— editable, runs on the server

Observa las dos comparaciones con ==. original y alias son literalmente el mismo objeto, por lo que la identidad se mantiene. original y upper tienen contenidos relacionados pero upper es un objeto nuevo — no hay forma de que upperCase haya podido modificar el que se le pasó. Esa es la garantía con la que cada desarrollador Java cuenta sin ni siquiera pensarlo.

Qué sigue

Cuando necesites un string que puedas modificar, la biblioteca estándar tiene un primo mutable de String. Es la pieza clave detrás de cada cadena + que el compilador optimiza, y la respuesta correcta cuando de otro modo recurrirías a += en un bucle. Continúa en Java StringBuilder.

Práctica

Práctica
¿Cuál de las siguientes opciones **no** es un beneficio de que `String` sea inmutable en Java?
¿Cuál de las siguientes opciones **no** es un beneficio de que `String` sea inmutable en Java?
Was this page helpful?