que - ¿Es una cadena de Java realmente inmutable?
pasar cadena a array java (14)
Todos sabemos que String
es 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é se cambia el valor de s1
y s2
, pero no s3
?
¿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.
Cita de la representación de cadena interna de Oracle Tunes Java :
En el nuevo paradigma, los campos String offset y count se han eliminado, por lo que las subcadenas ya no comparten el valor char [] subyacente.
Con este cambio, puede ocurrir sin reflexión (???).
De acuerdo con 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 (por ejemplo, M1).
Por otro lado, s3 contiene "Mundo", por lo que apuntará a una asignación de memoria diferente (por ejemplo, M2).
Entonces, ahora lo que está sucediendo es que el valor de S1 se está modificando (utilizando el valor char []). Por lo tanto, el valor en la ubicación de memoria M1 apuntada por s1 y s2 se ha cambiado.
Por lo tanto, como resultado, la ubicación de la 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.
En Java, si dos variables primitivas de cadena se inicializan al mismo literal, asigna la misma referencia a ambas variables:
String Test1="Hello World";
String Test2="Hello World";
System.out.println(test1==test2); // true
Esa es la razón por la cual la comparación devuelve verdadero. La tercera cadena se crea utilizando una substring()
que crea una nueva cadena en lugar de apuntar a la misma.
Cuando accede a una cadena usando la reflexión, obtiene el puntero real:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
Así que cambiar a esto cambiará la cadena que contiene un puntero, pero como s3
se crea con una nueva cadena debido a la substring()
, no cambiará.
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.
Está utilizando la reflexión para eludir la inmutabilidad de la cadena, es una forma de "ataque".
Hay muchos ejemplos que puedes crear de esta manera (por ejemplo , incluso puedes crear una instancia de un objeto Void
), pero eso no significa que String no sea "inmutable".
Hay casos de uso en los que este tipo de código se puede usar 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.
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.
La inmutabilidad de las cuerdas es desde la perspectiva de la interfaz. Está utilizando la reflexión para omitir la interfaz y modificar directamente los elementos internos de las instancias de String.
s1
y s2
se cambian porque están asignados a la misma instancia de cadena "interna". Puede descubrir un poco más sobre esa parte en este artículo sobre la igualdad de cadenas y la pasantía. Es posible que se sorprenda al descubrir que en su código de muestra, s1 == s2
devuelve true
!
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 (utilizando Arrays.copyOfRange ()).
s1 y s2 son iguales porque en Java se refieren a la misma cadena internada. Es por diseño en Java.
Las cadenas se crean en el área permanente de la memoria de almacenamiento dinámico de 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 pila: 1. Generación joven 2. Generación antigua 3. Generación permanente.
Cuando se crea un objeto, entra en el área de almacenamiento dinámico de la generación joven y en el área de PermGen reservada para la agrupación de cadenas.
Aquí hay más detalles de los que puede ir y obtener más información de: Cómo funciona la recolección de basura en Java .
Los modificadores de la visibilidad y el final (es decir, la 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 los detalles de la implementación interna como la matriz de caracteres de respaldo para las String
a través de la reflexión.
El segundo efecto que ve es que todas las String
cambian mientras parece que solo cambia s1
. Una de las propiedades de los literales de Java String es que son internados automáticamente, es decir, almacenados en caché. Dos literales de cadena con el mismo valor serán en realidad el mismo objeto. Cuando cree una cadena con una new
, no se internará automáticamente y no verá este efecto.
#substring
hasta 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, sino que reutilizó la de la cadena original; acaba de crear un nuevo objeto String que usaba un desplazamiento y una longitud para presentar solo una parte de esa matriz. Esto generalmente funciona, ya que las cuerdas son inmutables, a menos que lo evites. Esta propiedad de #substring
también significaba que toda la Cadena original no podía ser recogida de basura cuando todavía existía una subcadena más corta creada a partir de ella.
A partir de la versión actual de Java y su versión actual de la pregunta, no hay un comportamiento extraño de #substring
.
Para agregar a la respuesta de @haraldK, este es un truco de seguridad que podría tener un impacto grave 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 una String s = "Hello World";
, se está colocando en un grupo de objetos especiales para una mayor reutilización potencial. 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 resultaría en un error siguiente:
System.out.println("Hello World");
Se imprimirá:
Hello Java!
Hubo otro problema que experimenté cuando estaba implementando un cálculo pesado sobre tales cadenas de riesgo. Hubo un error que ocurrió en 1 de cada 1000000 veces durante el cómputo que hizo que el resultado no fuera determinista. Pude encontrar el problema apagando el JIT. Siempre obtenía el mismo resultado con el JIT apagado. Mi conjetura es que la razón fue este truco de seguridad String que rompió algunos de los contratos de optimización JIT.
Realmente hay dos preguntas aquí:
- ¿Son realmente inmutables las cuerdas?
- ¿Por qué no se cambia s3?
Para el punto 1: a excepción de ROM, no hay memoria inmutable en su computadora. Hoy en día, incluso ROM es a veces escribible. Siempre hay algún código en algún lugar (ya sea el kernel o el código nativo que elude su entorno administrado) que puede escribir en su dirección de memoria. Entonces, en la "realidad", no son absolutamente inmutables.
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 la subcadena de tal manera que no haga una copia, pero eso no significa que lo haga. Hay compensaciones involucradas.
Por ejemplo, ¿la retención de una referencia a reallyLargeString.substring(reallyLargeString.length - 2)
hace que una gran cantidad de memoria se mantenga viva, 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, a diferencia de 2 asignaciones de montón separadas.
En cualquier caso, parece que su JVM eligió usar copias en profundidad para las llamadas de subcadena.
[Descargo de responsabilidad: este es un estilo de respuesta deliberadamente opinada, ya que creo que está justificada una respuesta más de "no hagas esto en casa, niños"]
El pecado es la línea 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 se puede bloquear configurando un administrador de seguridad.
El fenómeno en la pregunta son los detalles de la implementación que nunca verías cuando no uses esa peligrosa línea de código para violar los modificadores de acceso a través de la reflexión. Claramente, dos (normalmente) cadenas inmutables pueden compartir la misma matriz de caracteres. Si una subcadena comparte la misma matriz depende de si puede y si el desarrollador pensó 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 detalles que no pueden experimentarse 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 esto, la línea de código es realmente muy útil cuando tienes un arma que te sostiene en la cabeza y te obliga a hacer cosas tan peligrosas. Usar esa puerta trasera suele ser un olor a código que necesita actualizar a un mejor código de biblioteca donde no tenga 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 con estos marcos (tanto a favor como en contra de ellos), por lo que evitaré invitar a una guerra de fuego diciendo que la gran mayoría de los programadores no tienen que ir allí.
String
es inmutable * pero esto solo significa que no puedes cambiarla usando su API pública.
Lo que está haciendo aquí es eludir la API normal, utilizando la reflexión. De la misma manera, puede cambiar los valores de las enumeraciones, cambiar la tabla de búsqueda utilizada en el autoboxing de enteros, etc.
Ahora, la razón por la que s1
y s2
cambian de valor, es que ambos se refieren a la misma cadena internada. El compilador hace esto (como lo mencionan otras respuestas).
La razón por la que s3
no me sorprendió, ya que pensé que compartiría la matriz de value
( lo hizo en una versión anterior de Java , antes de Java 7u6). Sin embargo, al observar el código fuente de String
, podemos ver que la matriz de caracteres de value
para una subcadena se copia realmente (utilizando Arrays.copyOfRange(..)
). Es por eso que no se modifica.
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 (normalmente herramientas ORM, bibliotecas AOP, etc.).
*) Inicialmente escribí que las String
no son realmente inmutables, simplemente "inmutables efectivas". Esto podría ser engañoso en la implementación actual de String
, donde la matriz de value
está marcada como private final
. Sin embargo, aún 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, he aquí algunas lecturas adicionales sugeridas: la Reflexión sobre la Locura de Reflexión de Heinz Kabutz de JavaZone 2009, que cubre muchos de los temas 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. :-)