Concorrência
Java foi definido desde o início com considerações de simultaneidade. Como frequentemente mencionado, os mutáveis compartilhados são problemáticos. Uma coisa pode mudar outra atrás da parte traseira de outro encadeamento sem que esse encadeamento esteja ciente disso.
Há uma série de erros de C ++ multithread que surgiram por causa de uma string compartilhada - onde um módulo achou seguro mudar quando outro módulo no código salvou um ponteiro e esperava que permanecesse o mesmo.
A 'solução' para isso é que toda classe faz uma cópia defensiva dos objetos mutáveis que são passados para ela. Para cadeias mutáveis, é O (n) para fazer a cópia. Para seqüências imutáveis, fazer uma cópia é O (1) porque não é uma cópia, é o mesmo objeto que não pode ser alterado.
Em um ambiente multithread, objetos imutáveis sempre podem ser compartilhados com segurança entre si. Isso leva a uma redução geral no uso de memória e melhora o cache de memória.
Segurança
Muitas vezes, as strings são passadas como argumentos para os construtores - conexões de rede e protocolos são os dois que mais facilmente vêm à mente. Ser capaz de alterar isso em um tempo indeterminado posteriormente na execução pode levar a problemas de segurança (a função pensou que estava se conectando a uma máquina, mas foi desviada para outra, mas tudo no objeto parece estar conectado à primeira ... é até a mesma string).
Java permite usar a reflexão - e os parâmetros para isso são cadeias de caracteres. O perigo de alguém passar uma string que pode ser modificada pelo caminho para outro método que reflete. Isso é muito ruim.
Chaves para o Hash
A tabela de hash é uma das estruturas de dados mais usadas. As chaves da estrutura de dados são muitas vezes cadeias de caracteres. Ter cadeias imutáveis significa que (como acima) a tabela de hash não precisa fazer uma cópia da chave de hash a cada vez. Se as strings fossem mutáveis e a tabela de hash não fizesse isso, seria possível que algo alterasse a chave de hash à distância.
A maneira como o Objeto em java funciona é que tudo tem uma chave de hash (acessada pelo método hashCode ()). Ter uma sequência imutável significa que o hashCode pode ser armazenado em cache. Considerando a frequência com que Strings são usadas como chaves para um hash, isso fornece um aumento significativo no desempenho (em vez de ter que recalcular o código de hash toda vez).
Substrings
Por ter a String imutável, a matriz de caracteres subjacente que suporta a estrutura de dados também é imutável. Isso permite certas otimizações no substring
método a ser feito (elas não são necessariamente feitas - também introduz a possibilidade de alguns vazamentos de memória).
Se você fizer:
String foo = "smiles";
String bar = foo.substring(1,5);
O valor de bar
é 'milha'. No entanto, ambos foo
e bar
podem ser apoiados pela mesma matriz de caracteres, reduzindo a instanciação de mais matrizes de caracteres ou copiando-as - apenas usando diferentes pontos de início e de final na sequência.
foo | | (0, 6)
vv
sorrisos
^ ^
bar | | (1, 5)
Agora, a desvantagem disso (o vazamento de memória) é que, se alguém tivesse uma sequência de 1k de comprimento e pegasse a substring do primeiro e do segundo caracteres, ele também seria apoiado pela matriz de caracteres de 1k de comprimento. Essa matriz permaneceria na memória mesmo se a cadeia original que tivesse um valor de toda a matriz de caracteres fosse coletada como lixo.
Pode-se ver isso em String do JDK 6b14 (o código a seguir é de uma fonte GPL v2 e é usado como exemplo)
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 como a substring usa o construtor String no nível do pacote que não envolve nenhuma cópia da matriz e seria muito mais rápido (às custas de possivelmente manter algumas matrizes grandes - embora também não duplicando matrizes grandes).
Observe que o código acima é para Java 1.6. A maneira como o construtor de substring é implementado foi alterado no Java 1.7, conforme documentado em Alterações na representação interna da String feita no Java 1.7.0_06
- o problema relacionado ao vazamento de memória que mencionei acima. Provavelmente o Java não era visto como uma linguagem com muita manipulação de String e, portanto, o aumento de desempenho para uma substring foi uma coisa boa. Agora, com enormes documentos XML armazenados em seqüências de caracteres que nunca são coletadas, isso se torna um problema ... e, portanto, a mudança para String
não usar a mesma matriz subjacente com uma substring, para que a matriz de caracteres maior possa ser coletada mais rapidamente.
Não abuse da pilha
Um poderia passar o valor da corda em torno em vez da referência para a seqüência imutável para evitar problemas com a mutabilidade. No entanto, com cadeias grandes, passar isso na pilha seria ... abusivo para o sistema (colocar documentos xml inteiros como cadeias na pilha e depois retirá-los ou continuar a transmiti-los ...).
A possibilidade de desduplicação
É verdade que essa não foi uma motivação inicial para o motivo pelo qual as Strings deveriam ser imutáveis, mas quando se olha para o racional do motivo pelo qual as Strings imutáveis são boas, certamente é algo a considerar.
Qualquer um que tenha trabalhado um pouco com Strings sabe que pode sugar memória. Isso é especialmente verdade quando você faz coisas como extrair dados de bancos de dados que ficam por um tempo. Muitas vezes com essas picadas, elas são a mesma string repetidamente (uma vez para cada linha).
Atualmente, muitos aplicativos Java de larga escala estão com gargalo na memória. As medidas mostraram que aproximadamente 25% dos dados dinâmicos do heap Java nesses tipos de aplicativos são consumidos pelos objetos String. Além disso, aproximadamente metade desses objetos String são duplicados, onde duplicados significa que string1.equals (string2) é verdadeira. Ter objetos String duplicados no heap é, essencialmente, apenas um desperdício de memória. ...
Com a atualização 20 do Java 8, o JEP 192 (motivação citada acima) está sendo implementado para resolver isso. Sem entrar em detalhes de como a desduplicação de string funciona, é essencial que as próprias Strings sejam imutáveis. Você não pode deduplicar StringBuilders porque eles podem mudar e você não quer que alguém mude algo de baixo de você. Seqüências imutáveis (relacionadas a esse conjunto de cadeias) significam que você pode passar e, se encontrar duas cadeias iguais, pode apontar uma referência para outra e deixar que o coletor de lixo consuma o novo não utilizado.
Outras línguas
O objetivo C (que antecede Java) possui NSString
e NSMutableString
.
C # e .NET fizeram as mesmas escolhas de design da string padrão sendo imutável.
As strings de Lua também são imutáveis.
Python também.
Historicamente, Lisp, Scheme, Smalltalk todos internam a string e, portanto, fazem com que ela seja imutável. As linguagens dinâmicas mais modernas costumam usar strings de alguma maneira que exige que sejam imutáveis (pode não ser uma String , mas é imutável).
Conclusão
Essas considerações de design foram feitas repetidamente em vários idiomas. É o consenso geral de que seqüências imutáveis, apesar de todo o embaraço, são melhores que as alternativas e levam a um código melhor (menos bugs) e a executáveis mais rápidos em geral.