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é es el valor de s1
y s2
cambiado, pero no s3
?
java
string
reflection
immutability
Darshan Patel
fuente
fuente
(Integer)1+(Integer)2=42
jugando con el autoboxing en caché; (Disgruntled-Bomb-Java-Edition) ( thedailywtf.com/Articles/Disgruntled-Bomb-Java-Edition.aspx )Respuestas:
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
s1
y els2
valor 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
s3
qué no era en realidad un poco sorprendente para mí, ya que pensé que compartiría elvalue
array ( lo hizo en la versión anterior de Java , antes 7u6 de Java). Sin embargo, mirando el código fuente deString
, podemos ver que lavalue
matriz de caracteres para una subcadena se copia (usandoArrays.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
String
s no son realmente inmutables, solo "inmutables efectivos". Esto puede ser engañoso en la implementación actual deString
, donde lavalue
matriz está realmente marcadaprivate 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. :-)
fuente
String
internamiento 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 laString
clase.substring
copias en lugar de usar una "sección" de la matriz existente, es que si tuviera una gran cadenas
y sacara una pequeña subcadena llamadat
de ella, y luego abandonaras
pero mantuvierat
, 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?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.byte[]
cadenas para cadenas ASCII ychar[]
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.En Java, si dos variables primitivas de cadena se inicializan en el mismo literal, asigna la misma referencia a ambas variables:
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.Cuando accede a una cadena usando la reflexión, obtiene el puntero real:
Entonces, cambiar a esto cambiará la cadena que contiene un puntero, pero como
s3
se crea con una nueva cadena debido asubstring()
que no cambiaría.fuente
intern
manualmente en una cadena no literal y cosechar los beneficios.intern
prudencia. Practicar todo no te gana mucho, y puede ser la fuente de algunos momentos de rascarse la cabeza cuando agregas reflejo a la mezcla.Test1
yTest1
son inconsistentes contest1==test2
y no siguen las convenciones de nomenclatura de Java.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
Void
objeto 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.
fuente
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.
fuente
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
String
s mediante la reflexión.El segundo efecto que ves es que todo
String
cambia mientras parece que solo cambiass1
. 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 connew
ella, 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, 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#substring
tambié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
.fuente
final
ni siquiera a través de la reflexión. Además, como se menciona en otra respuesta, desde Java 7u6,#substring
no comparte matrices.final
ha cambiado con el tiempo ...: -O Según la charla "Reflection Madness" de Heinz que publiqué en el otro hilo,final
significaba 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 ...final
los campos se pueden cambiar a través delnative
código como lo hace el marco de serialización al leer los campos de una instancia serializada, así como también loSystem.setOut(…)
que modifica laSystem.out
variable final . Esta última es la característica más interesante ya que la reflexión con anulación de acceso no puede cambiar losstatic final
campos.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.
s1
ys2
ambos 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 == s2
regresatrue
!fuente
¿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 :
Con este cambio, puede suceder sin reflexión (???).
fuente
Realmente hay dos preguntas aquí:
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.
fuente
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:Imprimirá:
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.
fuente
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.String
internas, ¿no se te ocurrió?final
garantí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 piratearString
y modificar los datos que se espera sean inmutables.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.
fuente
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.
fuente
String.substring(int, int)
cambiado con Java 7u6. Antes 7u6, la JVM sería simplemente mantener un puntero a la originalString
'schar[]
junto con un índice y longitud. Después de 7u6, copia la subcadena en una nuevaString
Hay ventajas y desventajas.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.
fuente
[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í.
fuente
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 .
fuente
La naturaleza de la cadena es inmutable porque no hay ningún método para modificar el objeto de la cadena. Esa es la razón por la que introdujeron las clases StringBuilder y StringBuffer
fuente