Concurrencia
Java se definió desde el principio con consideraciones de concurrencia. Como se ha mencionado a menudo, las mutables compartidas son problemáticas. Una cosa puede cambiar a otra detrás de otro hilo sin que ese hilo sea consciente de ello.
Hay una gran cantidad de errores de C ++ multiproceso que se han creado debido a una cadena compartida, donde un módulo pensó que era seguro cambiar cuando otro módulo en el código había guardado un puntero y esperaba que permaneciera igual.
La 'solución' a esto es que cada clase hace una copia defensiva de los objetos mutables que se le pasan. Para cadenas mutables, esto es O (n) para hacer la copia. Para cadenas inmutables, hacer una copia es O (1) porque no es una copia, es el mismo objeto que no puede cambiar.
En un entorno multiproceso, los objetos inmutables siempre se pueden compartir de forma segura entre sí. Esto conduce a una reducción general en el uso de memoria y mejora el almacenamiento en memoria caché.
Seguridad
Muchas veces las cadenas se pasan como argumentos a los constructores: las conexiones de red y los protocolos son los dos que más fácilmente se nos ocurren. Ser capaz de cambiar esto en un momento indeterminado más adelante en la ejecución puede conducir a problemas de seguridad (la función pensó que se estaba conectando a una máquina, pero se desvió a otra, pero todo en el objeto parece estar conectado a la primera ... es incluso la misma cadena).
Java permite usar la reflexión, y los parámetros para esto son cadenas. El peligro de que alguien pase una cadena que puede modificarse a otro método que refleja. Esto es muy malo.
Claves para el hash
La tabla hash es una de las estructuras de datos más utilizadas. Las claves de la estructura de datos suelen ser cadenas. Tener cadenas inmutables significa que (como arriba) la tabla hash no necesita hacer una copia de la clave hash cada vez. Si las cadenas fueran mutables, y la tabla hash no hiciera esto, sería posible que algo cambiara la clave hash a distancia.
La forma en que funciona el Object en java es que todo tiene una clave hash (a la que se accede mediante el método hashCode ()). Tener una cadena inmutable significa que el código hash se puede almacenar en caché. Teniendo en cuenta la frecuencia con la que las cadenas se usan como claves para un hash, esto proporciona un aumento significativo del rendimiento (en lugar de tener que volver a calcular el código hash cada vez).
Subcadenas
Al hacer que la cadena sea inmutable, la matriz de caracteres subyacente que respalda la estructura de datos también es inmutable. Esto permite ciertas optimizaciones en el substring
método que se debe hacer (no necesariamente se hace, también presenta la posibilidad de algunas pérdidas de memoria también).
Si lo haces:
String foo = "smiles";
String bar = foo.substring(1,5);
El valor de bar
es 'milla'. Sin embargo, tanto foo
y bar
pueda ir acompañado de la misma matriz de caracteres, lo que reduce la creación de instancias de más matrices de caracteres o copiarlo - simplemente utilizando diferentes puntos de inicio y fin dentro de la cadena.
foo | El | (0, 6)
vv
sonrisas
^ ^
bar | El | (15)
Ahora, la desventaja de eso (la pérdida de memoria) es que si uno tuviera una cadena de 1k de largo y tomara la subcadena del primer y segundo carácter, también estaría respaldada por la matriz de caracteres de 1k de largo. Esta matriz permanecería en la memoria incluso si la cadena original que tenía un valor de la matriz de caracteres completa se recolectara basura.
Uno puede ver esto en String from JDK 6b14 (el siguiente código es de una fuente GPL v2 y se usa como ejemplo)
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.offset = 0;
this.count = count;
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
// Package private constructor which shares value array for speed.
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
Observe cómo la subcadena utiliza el constructor de cadenas de nivel de paquete que no implica ninguna copia de la matriz y sería mucho más rápido (a expensas de posiblemente mantener algunas matrices grandes, aunque tampoco duplicar matrices grandes).
Tenga en cuenta que el código anterior es para Java 1.6. La forma en que se implementa el constructor de subcadenas se modificó con Java 1.7, tal como se documenta en la representación interna Cambios en la cadena realizada en Java 1.7.0_06
, el problema relacionado con la pérdida de memoria que mencioné anteriormente. Es probable que Java no se haya visto como un lenguaje con mucha manipulación de cadenas, por lo que el aumento de rendimiento para una subcadena fue algo bueno. Ahora, con enormes documentos XML almacenados en cadenas que nunca se recopilan, esto se convierte en un problema ... y, por lo tanto, el cambio a String
no usar la misma matriz subyacente con una subcadena, para que la matriz de caracteres más grande se pueda recopilar más rápidamente.
No abuses de la pila
Se podría pasar el valor de la cadena en lugar de la referencia a la cadena inmutable para evitar problemas con la mutabilidad. Sin embargo, con cadenas grandes, pasar esto en la pila sería ... abusivo para el sistema (colocar documentos xml completos como cadenas en la pila y luego quitarlos o continuar pasándolos ...).
La posibilidad de deduplicación
De acuerdo, esto no fue una motivación inicial de por qué las cadenas deberían ser inmutables, pero cuando uno está mirando la razón de por qué las cadenas inmutables son algo bueno, esto es ciertamente algo a considerar.
Cualquiera que haya trabajado un poco con Strings sabe que puede succionar memoria. Esto es especialmente cierto cuando estás haciendo cosas como extraer datos de bases de datos que se quedan por un tiempo. Muchas veces con estas picaduras, son la misma cadena una y otra vez (una vez para cada fila).
Actualmente, muchas aplicaciones Java a gran escala tienen cuellos de botella en la memoria. Las mediciones han demostrado que aproximadamente el 25% del conjunto de datos dinámicos de almacenamiento dinámico de Java en este tipo de aplicaciones es consumido por objetos String. Además, aproximadamente la mitad de esos objetos String son duplicados, donde duplicados significa string1.equals (string2) es verdadero. Tener duplicados objetos String en el montón es, esencialmente, solo un desperdicio de memoria. ...
Con Java 8 actualización 20, JEP 192 (motivación citada anteriormente) se está implementando para abordar esto. Sin entrar en detalles sobre cómo funciona la deduplicación de cadenas, es esencial que las cadenas mismas sean inmutables. No puede deduplicar StringBuilders porque pueden cambiar y no desea que alguien cambie algo debajo de usted. Las cadenas inmutables (relacionadas con ese grupo de cadenas) significa que puede pasar y si encuentra dos cadenas que son iguales, puede apuntar una referencia de cadena a la otra y dejar que el recolector de basura consuma el nuevo no utilizado.
Otros idiomas
El objetivo C (que precede a Java) tiene NSString
y NSMutableString
.
C # y .NET tomaron las mismas opciones de diseño de la cadena predeterminada, que es inmutable.
Las cuerdas de Lua también son inmutables.
Python también.
Históricamente, Lisp, Scheme, Smalltalk todos internan la cadena y, por lo tanto, hacen que sea inmutable. Los lenguajes dinámicos más modernos a menudo usan cadenas de alguna manera que requiere que sean inmutables (puede que no sea una cadena , pero es inmutable).
Conclusión
Estas consideraciones de diseño se han hecho una y otra vez en una multitud de idiomas. Es el consenso general de que las cadenas inmutables, a pesar de su incomodidad, son mejores que las alternativas y conducen a un mejor código (menos errores) y ejecutables más rápidos en general.