¿Es una cadena Java realmente inmutable?


399

Todos sabemos que Stringes inmutable en Java, pero verifique el siguiente código:

String s1 = "Hello World";  
String s2 = "Hello World";  
String s3 = s1.substring(6);  
System.out.println(s1); // Hello World  
System.out.println(s2); // Hello World  
System.out.println(s3); // World  

Field field = String.class.getDeclaredField("value");  
field.setAccessible(true);  
char[] value = (char[])field.get(s1);  
value[6] = 'J';  
value[7] = 'a';  
value[8] = 'v';  
value[9] = 'a';  
value[10] = '!';  

System.out.println(s1); // Hello Java!  
System.out.println(s2); // Hello Java!  
System.out.println(s3); // World  

¿Por qué este programa funciona así? Y por qué es el valor de s1y s2cambiado, pero no s3?


394
Puedes hacer todo tipo de trucos estúpidos con reflexión. Pero básicamente está rompiendo la etiqueta de "garantía nula si se elimina" en la clase en el instante en que lo hace.
cHao

16
@DarshanPatel utiliza un SecurityManager para desactivar la reflexión
Sean Patrick Floyd

39
Si realmente quiere meterse con cosas, puede hacerlo (Integer)1+(Integer)2=42jugando con el autoboxing en caché; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )
Richard Tingle

15
Puede que te divierta esta respuesta que escribí hace casi 5 años stackoverflow.com/a/1232332/27423 : se trata de listas inmutables en C #, pero es básicamente lo mismo: ¿cómo puedo evitar que los usuarios modifiquen mis datos? Y la respuesta es que no puedes; La reflexión lo hace muy fácil. Un lenguaje convencional que no tiene este problema es JavaScript, ya que no tiene un sistema de reflexión que pueda acceder a las variables locales dentro de un cierre, por lo que privado realmente significa privado (¡aunque no haya una palabra clave para ello!)
Daniel Earwicker

49
¿Alguien está leyendo la pregunta hasta el final? La pregunta es, déjenme repetir: "¿Por qué este programa funciona así? ¿Por qué se cambia el valor de s1 y s2 y no se cambia para s3?" ¡La pregunta NO es por qué se cambian s1 y s2! La pregunta ES: ¿POR QUÉ no se cambia s3?
Roland Pihlakas

Respuestas:


403

String es inmutable *, pero esto solo significa que no puede cambiarlo utilizando su API pública.

Lo que está haciendo aquí es eludir la API normal, utilizando la reflexión. Del mismo modo, puede cambiar los valores de las enumeraciones, cambiar la tabla de búsqueda utilizada en el autoboxing de enteros, etc.

Ahora, la razón s1y el s2valor de cambio, es que ambos se refieren a la misma cadena interna. El compilador hace esto (como se menciona en otras respuestas).

La razón por s3qué no era en realidad un poco sorprendente para mí, ya que pensé que compartiría el valuearray ( lo hizo en la versión anterior de Java , antes 7u6 de Java). Sin embargo, mirando el código fuente de String, podemos ver que la valuematriz de caracteres para una subcadena se copia (usando Arrays.copyOfRange(..)). Es por eso que no cambia.

Puede instalar un SecurityManager, para evitar el código malicioso para hacer tales cosas. Pero tenga en cuenta que algunas bibliotecas dependen del uso de este tipo de trucos de reflexión (generalmente herramientas ORM, bibliotecas AOP, etc.).

*) Inicialmente escribí que los Strings no son realmente inmutables, solo "inmutables efectivos". Esto puede ser engañoso en la implementación actual de String, donde la valuematriz está realmente marcada private final. Sin embargo, todavía vale la pena señalar que no hay forma de declarar una matriz en Java como inmutable, por lo que se debe tener cuidado de no exponerla fuera de su clase, incluso con los modificadores de acceso adecuados.


Como este tema parece abrumadoramente popular, aquí hay algunas lecturas adicionales sugeridas: la charla Reflection Madness de Heinz Kabutz de JavaZone 2009, que cubre muchos de los problemas en el OP, junto con otra reflexión ... bueno ... locura.

Cubre por qué esto a veces es útil. Y por qué, la mayoría de las veces, debes evitarlo. :-)


77
En realidad, el Stringinternamiento es parte de JLS ( "un literal de cadena siempre se refiere a la misma instancia de la clase String" ). Pero estoy de acuerdo, no es una buena práctica contar con los detalles de implementación de la Stringclase.
haraldK

3
Tal vez la razón por la cual las substringcopias en lugar de usar una "sección" de la matriz existente, es que si tuviera una gran cadena sy sacara una pequeña subcadena llamada tde ella, y luego abandonara spero mantuviera t, entonces la gran matriz se mantendría viva (no basura recolectada). Entonces, ¿quizás es más natural que cada valor de cadena tenga su propia matriz asociada?
Jeppe Stig Nielsen

10
Compartir matrices entre una cadena y sus subcadenas también implicaba que cada String instancia tenía que llevar variables para recordar el desplazamiento en la matriz y longitud referidas. Esa es una sobrecarga que no se debe ignorar dada la cantidad total de cadenas y la relación típica entre cadenas y subcadenas normales en una aplicación. Como tenían que ser evaluados para cada operación de cadena, significaba reducir la velocidad cada operación de cadena solo en beneficio de una sola operación, una subcadena barata.
Holger

2
@Holger - Sí, entiendo que el campo de compensación se eliminó en las JVM recientes. E incluso cuando estaba presente, no se usaba con tanta frecuencia.
Hot Licks el

2
@supercat: no importa si tiene código nativo o no, tener implementaciones diferentes para cadenas y subcadenas dentro de la misma JVM o tener byte[]cadenas para cadenas ASCII y char[]para otros implica que cada operación tiene que verificar qué tipo de cadena es antes operando. Esto dificulta la inserción del código en los métodos que utilizan cadenas, que es el primer paso para otras optimizaciones que utilizan la información de contexto del llamante. Este es un gran impacto.
Holger

93

En Java, si dos variables primitivas de cadena se inicializan en el mismo literal, asigna la misma referencia a ambas variables:

String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true

inicialización

Esa es la razón por la cual la comparación devuelve verdadero. La tercera cadena se crea usando substring()una nueva cadena en lugar de apuntar a la misma.

subcadena

Cuando accede a una cadena usando la reflexión, obtiene el puntero real:

Field field = String.class.getDeclaredField("value");
field.setAccessible(true);

Entonces, cambiar a esto cambiará la cadena que contiene un puntero, pero como s3se crea con una nueva cadena debido a substring()que no cambiaría.

cambio


Esto solo funciona para literales y es una optimización en tiempo de compilación.
SpacePrez

2
@ Zaphod42 No es cierto. También puede llamar internmanualmente en una cadena no literal y cosechar los beneficios.
Chris Hayes

Tenga en cuenta, sin embargo: desea utilizar con internprudencia. Practicar todo no te gana mucho, y puede ser la fuente de algunos momentos de rascarse la cabeza cuando agregas reflejo a la mezcla.
cHao

Test1y Test1son inconsistentes con test1==test2y no siguen las convenciones de nomenclatura de Java.
c0der

50

Estás utilizando la reflexión para eludir la inmutabilidad de String: es una forma de "ataque".

Hay muchos ejemplos que puede crear de esta manera (por ejemplo , incluso puede crear una instancia de un Voidobjeto también), pero eso no significa que String no sea "inmutable".

Hay casos de uso en los que este tipo de código puede usarse para su ventaja y puede ser una "buena codificación", como borrar las contraseñas de la memoria lo antes posible (antes de GC) .

Dependiendo del administrador de seguridad, es posible que no pueda ejecutar su código.


30

Está utilizando la reflexión para acceder a los "detalles de implementación" del objeto de cadena. La inmutabilidad es la característica de la interfaz pública de un objeto.


24

Los modificadores de visibilidad y final (es decir, inmutabilidad) no son una medida contra el código malicioso en Java; son simplemente herramientas para protegerse contra errores y hacer que el código sea más fácil de mantener (uno de los grandes puntos de venta del sistema). Es por eso que puede acceder a detalles de implementación internos como la matriz de caracteres de respaldo para Strings mediante la reflexión.

El segundo efecto que ves es que todo Stringcambia mientras parece que solo cambias s1. Es una cierta propiedad de los literales de Java String que se internan automáticamente, es decir, se almacenan en caché. Dos literales de cadena con el mismo valor en realidad serán el mismo objeto. Cuando cree una Cadena con newella, no se internará automáticamente y no verá este efecto.

#substringhasta hace poco (Java 7u6) funcionaba de manera similar, lo que habría explicado el comportamiento en la versión original de su pregunta. No creó una nueva matriz de caracteres de respaldo, pero reutilizó la del String original; acaba de crear un nuevo objeto String que utiliza un desplazamiento y una longitud para presentar solo una parte de esa matriz. Esto generalmente funcionó ya que las cadenas son inmutables, a menos que lo evite. Esta propiedad de #substringtambién significaba que no se podía recolectar toda la cadena original cuando todavía existía una subcadena más corta creada a partir de ella.

A partir de Java actual y su versión actual de la pregunta, no hay un comportamiento extraño de #substring.


2
En realidad, los modificadores de visibilidad son (o al menos fueron) como protección contra el código malicioso; sin embargo, debe configurar un SecurityManager (System.setSecurityManager ()) para activar la protección. Qué tan seguro es esto en realidad es otra pregunta ...
sleske

2
Merece un voto positivo porque enfatiza que los modificadores de acceso no están destinados a 'proteger' el código. Esto parece ser ampliamente malentendido tanto en Java como en .NET. Aunque el comentario anterior contradice eso; No sé mucho sobre Java, pero en .NET esto es ciertamente cierto. En ninguno de los dos idiomas los usuarios deben asumir que esto hace que su código sea a prueba de piratería.
Tom W

No es posible violar el contrato finalni siquiera a través de la reflexión. Además, como se menciona en otra respuesta, desde Java 7u6, #substringno comparte matrices.
ntoskrnl

En realidad, el comportamiento de finalha cambiado con el tiempo ...: -O Según la charla "Reflection Madness" de Heinz que publiqué en el otro hilo, finalsignificaba final en JDK 1.1, 1.3 y 1.4, pero podría modificarse usando la reflexión siempre usando 1.2 , y en 1.5 y 6 en la mayoría de los casos ...
haraldK

1
finallos campos se pueden cambiar a través del nativecódigo como lo hace el marco de serialización al leer los campos de una instancia serializada, así como también lo System.setOut(…)que modifica la System.outvariable final . Esta última es la característica más interesante ya que la reflexión con anulación de acceso no puede cambiar los static finalcampos.
Holger

11

La inmutabilidad de la cadena es desde la perspectiva de la interfaz. Está utilizando la reflexión para omitir la interfaz y modificar directamente las partes internas de las instancias de String.

s1y s2ambos se modifican porque ambos están asignados a la misma instancia de cadena "interna". Puede encontrar un poco más sobre esa parte de este artículo sobre igualdad de cadenas e internado. Puede que se sorprenda al descubrir que en su código de muestra, ¡ s1 == s2regresa true!


10

¿Qué versión de Java estás usando? Desde Java 1.7.0_06, Oracle ha cambiado la representación interna de String, especialmente la subcadena.

Citando la representación de cadenas internas de Oracle Tunes Java :

En el nuevo paradigma, los campos de recuento y recuento de cadenas se han eliminado, por lo que las subcadenas ya no comparten el valor char [] subyacente.

Con este cambio, puede suceder sin reflexión (???).


2
Si el OP estaba utilizando un Sun / Oracle JRE anterior, la última declaración imprimiría "Java!" (como lo publicó accidentalmente). Esto solo afecta el intercambio de la matriz de valores entre cadenas y subcadenas. Todavía no puede cambiar el valor sin trucos, como la reflexión.
haraldK

7

Realmente hay dos preguntas aquí:

  1. ¿Son las cuerdas realmente inmutables?
  2. ¿Por qué no se cambia s3?

Al punto 1: Excepto para ROM, no hay memoria inmutable en su computadora. Hoy en día, incluso la ROM a veces se puede escribir. Siempre hay algún código en algún lugar (ya sea el núcleo o el código nativo que elude su entorno administrado) que puede escribir en su dirección de memoria. Entonces, en la "realidad", no, no son absolutamente inmutables.

Al punto 2: Esto se debe a que la subcadena probablemente esté asignando una nueva instancia de cadena, que probablemente esté copiando la matriz. Es posible implementar una subcadena de tal manera que no haga una copia, pero eso no significa que lo haga. Hay compensaciones involucradas.

Por ejemplo, ¿debería contener una referencia para reallyLargeString.substring(reallyLargeString.length - 2)hacer que se mantenga viva una gran cantidad de memoria o solo unos pocos bytes?

Eso depende de cómo se implemente la subcadena. Una copia profunda mantendrá menos memoria viva, pero se ejecutará un poco más lento. Una copia superficial mantendrá más memoria viva, pero será más rápida. El uso de una copia profunda también puede reducir la fragmentación del montón, ya que el objeto de cadena y su búfer se pueden asignar en un bloque, en lugar de 2 asignaciones de montón separadas.

En cualquier caso, parece que su JVM eligió usar copias profundas para llamadas de subcadenas.


3
La ROM real es tan inmutable como una impresión fotográfica encerrada en plástico. El patrón se establece permanentemente cuando la oblea (o impresión) se desarrolla químicamente. Las memorias que pueden modificarse eléctricamente, incluidos los chips RAM , pueden comportarse como ROM "verdaderas" si las señales de control necesarias para escribir no pueden activarse sin agregar conexiones eléctricas adicionales al circuito en el que está instalado. En realidad, no es raro que los dispositivos integrados incluyan RAM, que se configura en la fábrica y se mantiene con una batería de respaldo, y cuyo contenido debería ser recargado por la fábrica si falla la batería.
supercat

3
@supercat: Sin embargo, su computadora no es uno de esos sistemas integrados. :) Las verdaderas ROM cableadas no han sido comunes en las PC durante una década o dos; todo es EEPROM y flash en estos días. Básicamente, cada dirección visible para el usuario que se refiere a la memoria, se refiere a la memoria potencialmente grabable.
cHao

@cHao: Muchos chips flash permiten que las porciones estén protegidas contra escritura de una manera que, si se puede deshacer, requeriría aplicar voltajes diferentes de los que se requerirían para el funcionamiento normal (que las placas base no estarían equipadas para hacer). Esperaría que las placas base utilicen esa función. Además, no estoy seguro acerca de las computadoras de hoy, pero históricamente algunas computadoras han tenido una región de RAM que estaba protegida contra escritura durante la etapa de arranque y solo podían desprotegerse mediante un reinicio (lo que forzaría la ejecución para comenzar desde la ROM).
supercat

2
@supercat Creo que te estás perdiendo el punto del tema, que es que las cadenas, almacenadas en la RAM, nunca serán realmente inmutables.
Scott Wisniewski

5

Para agregar a la respuesta de @ haraldK, este es un truco de seguridad que podría tener un grave impacto en la aplicación.

Lo primero es una modificación a una cadena constante almacenada en un conjunto de cadenas. Cuando la cadena se declara como a String s = "Hello World";, se coloca en un grupo de objetos especiales para una posible reutilización adicional. El problema es que el compilador colocará una referencia a la versión modificada en el momento de la compilación y una vez que el usuario modifique la cadena almacenada en este grupo en tiempo de ejecución, todas las referencias en el código apuntarán a la versión modificada. Esto daría lugar a un siguiente error:

System.out.println("Hello World"); 

Imprimirá:

Hello Java!

Hubo otro problema que experimenté cuando estaba implementando un cálculo pesado sobre cadenas tan arriesgadas. Hubo un error que ocurrió como 1 de cada 1000000 veces durante el cálculo que hizo que el resultado fuera indeterminado. Pude encontrar el problema apagando el JIT: siempre obtenía el mismo resultado con JIT apagado. Supongo que la razón fue este truco de seguridad de String que rompió algunos de los contratos de optimización JIT.


Podría haber sido un problema de seguridad de subprocesos que fue enmascarado por un tiempo de ejecución más lento y menos concurrencia sin JIT.
Ted Pennings

@TedPennings Desde mi descripción podría, simplemente no quería entrar demasiado en detalles. De hecho, pasé como un par de días tratando de localizarlo. Era un algoritmo de un solo hilo que calculaba una distancia entre dos textos escritos en dos idiomas diferentes. Encontré dos posibles soluciones para el problema: una era apagar el JIT y la segunda era agregar literalmente no-op String.format("")dentro de uno de los bucles internos. Existe la posibilidad de que sea un problema que no sea JIT, pero creo que fue JIT, porque este problema nunca se volvió a reproducir después de agregar este no-op.
Andrey Chaschev

Estaba haciendo esto con una versión anterior de JDK ~ 7u9, por lo que podría ser.
Andrey Chaschev

1
@Andrey Chaschev: "Encontré dos posibles soluciones para el problema" ... la tercera solución posible, no hackear las partes Stringinternas, ¿no se te ocurrió?
Holger

1
@Ted Pennings: los problemas de seguridad de subprocesos y los problemas JIT son a menudo los mismos El JIT puede generar código que se basa en las finalgarantías de seguridad de subprocesos de campo que se rompen al modificar los datos después de la construcción del objeto. Por lo tanto, puede verlo como un problema JIT o un problema MT tal como lo desee. El verdadero problema es piratear Stringy modificar los datos que se espera sean inmutables.
Holger

5

Según el concepto de agrupación, todas las variables de cadena que contienen el mismo valor apuntarán a la misma dirección de memoria. Por lo tanto, s1 y s2, ambos con el mismo valor de "Hello World", apuntarán hacia la misma ubicación de memoria (digamos M1).

Por otro lado, s3 contiene "Mundo", por lo tanto, apuntará a una asignación de memoria diferente (digamos M2).

Entonces, lo que está sucediendo es que el valor de S1 está siendo modificado (usando el valor char []). Por lo tanto, el valor en la ubicación de memoria M1 señalado por s1 y s2 ha cambiado.

Por lo tanto, como resultado, la ubicación de memoria M1 se ha modificado, lo que provoca un cambio en el valor de s1 y s2.

Pero el valor de la ubicación M2 permanece inalterado, por lo tanto, s3 contiene el mismo valor original.


5

La razón por la que s3 no cambia realmente es porque en Java cuando se hace una subcadena, la matriz de caracteres de valor para una subcadena se copia internamente (usando Arrays.copyOfRange ()).

s1 y s2 son iguales porque en Java ambos se refieren a la misma cadena interna. Es por diseño en Java.


2
¿Cómo esta respuesta agrega algo a las respuestas que tienes delante?
Gris

También tenga en cuenta que este es un comportamiento bastante nuevo, y no está garantizado por ninguna especificación.
Paŭlo Ebermann

La implementación de String.substring(int, int)cambiado con Java 7u6. Antes 7u6, la JVM sería simplemente mantener un puntero a la original String's char[]junto con un índice y longitud. Después de 7u6, copia la subcadena en una nueva StringHay ventajas y desventajas.
Eric Jablow el

2

La cadena es inmutable, pero a través de la reflexión se le permite cambiar la clase de cadena. Acaba de redefinir la clase String como mutable en tiempo real. Si lo desea, puede redefinir los métodos para que sean públicos, privados o estáticos.


2
Si cambia la visibilidad de los campos / métodos, no es útil porque en el momento de la compilación son privados
Bohemian

1
Puede cambiar la accesibilidad de los métodos, pero no puede cambiar su estado público / privado y no puede hacer que sean estáticos.
Gris

1

[Descargo de responsabilidad: este es un estilo de respuesta deliberadamente obstinado ya que siento que se justifica una respuesta más de "no hagas esto en casa, niños"]

El pecado es la linea field.setAccessible(true); que dice violar la API pública al permitir el acceso a un campo privado. Es un agujero de seguridad gigante que puede bloquearse configurando un administrador de seguridad.

El fenómeno en la pregunta son los detalles de implementación que nunca vería cuando no usa esa línea de código peligrosa para violar los modificadores de acceso a través de la reflexión. Claramente, dos cadenas (normalmente) inmutables pueden compartir la misma matriz de caracteres. Si una subcadena comparte la misma matriz depende de si puede y si el desarrollador pensó en compartirla. Normalmente, estos son detalles de implementación invisibles que no debería tener que saber a menos que dispare el modificador de acceso a través de la cabeza con esa línea de código.

Simplemente no es una buena idea confiar en esos detalles que no se pueden experimentar sin violar los modificadores de acceso mediante la reflexión. El propietario de esa clase solo admite la API pública normal y es libre de realizar cambios de implementación en el futuro.

Habiendo dicho todo eso, la línea de código es realmente muy útil cuando tienes una pistola en la cabeza que te obliga a hacer cosas tan peligrosas. El uso de esa puerta trasera suele ser un olor a código que necesita actualizar a un mejor código de biblioteca donde no tiene que pecar. Otro uso común de esa peligrosa línea de código es escribir un "marco vudú" (orm, contenedor de inyección, ...). Muchas personas se vuelven religiosas acerca de tales marcos (tanto a favor como en contra de ellos), así que evitaré invitar a una guerra de llamas al decir nada más que la gran mayoría de los programadores no tienen que ir allí.


1

Las cadenas se crean en el área permanente de la memoria de montón JVM. Entonces, sí, es realmente inmutable y no se puede cambiar después de ser creado. Porque en la JVM, hay tres tipos de memoria de almacenamiento dinámico: 1. Generación joven 2. Generación antigua 3. Generación permanente.

Cuando se crea cualquier objeto, entra en el área del montón de generación joven y el área de PermGen reservada para la agrupación de cadenas.

Aquí hay más detalles que puede obtener y obtener más información de: Cómo funciona la recolección de basura en Java .


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.