W3docs

Java String Pool

Como o Java String pool funciona, por que literais são internados e o método intern().

Um programa Java típico cria milhares de strings, e uma grande fração delas são os mesmos caracteres de alguma outra string em outra parte do programa. Nomes de métodos. Chaves de configuração. Mensagens de erro. Rótulos de campos. A JVM trata essa redundância como algo que vale a pena resolver — ela mantém uma região especial chamada string pool (ou tabela de internamento de strings) e dá a cada literal que aparece no código-fonte uma entrada compartilhada lá. Dois literais com os mesmos caracteres acabam apontando para o mesmo objeto.

Esse compartilhamento tem consequências visíveis para a comparação de identidade (==), o uso de memória e um pequeno número de bugs sutis em torno do intern(). Este capítulo trata das regras.

Literais são armazenados no pool, new String(...) não

O fato mais importante:

String a = "hello";
String b = "hello";
System.out.println(a == b);   // true — same pooled object

String c = new String("hello");
System.out.println(a == c);   // false — new String, fresh object

Cada literal de string que o compilador vê é adicionado ao pool na primeira vez que é carregado. Ocorrências subsequentes do mesmo literal — em qualquer lugar do programa, em qualquer classe — retornam a mesma referência. Portanto, a e b são o mesmo objeto.

new String("hello") força uma nova alocação no heap. O argumento "hello" ainda é armazenado no pool (porque é um literal), mas o construtor o copia para um objeto completamente novo fora do pool. c e a, portanto, têm conteúdo igual, mas identidades diferentes.

Essa é a razão pela qual "use equals, não ==" é ensinado em todos os livros de Java. A comparação de identidade funciona por acaso para literais simples, mas falha no momento em que uma string vem de new, de um parser, de entrada de rede ou de uma concatenação que o compilador não dobrou em tempo de compilação.

O que vive no pool

O pool é populado por duas rotas:

  1. Literais de string no código-fonte. O compilador emite cada literal único como uma entrada CONSTANT_String no pool de constantes da classe; a JVM o resolve em um objeto String real no pool residente no heap na primeira vez que a classe o utiliza.
  2. Chamadas explícitas a intern(). Qualquer String para a qual você tenha uma referência pode ser adicionada ao pool chamando s.intern(). O método retorna a instância do pool — que é a mesma referência para todo chamador que interna conteúdos iguais.

Strings computadas — a + b, s.substring(...), resultados de String.formatnão são armazenadas no pool automaticamente. Elas vivem onde o GC as colocou e têm a identidade que acaso possuem.

String x = "java";
String y = "ja" + "va";          // compile-time constant — pooled, == x
String z = "ja" + new String("va"); // runtime computation — NOT pooled
System.out.println(x == y);      // true
System.out.println(x == z);      // false
System.out.println(x == z.intern()); // true — intern() returns the pooled instance

O segundo caso é a armadilha. y é computado a partir de dois literais, mas o compilador dobra a concatenação em tempo de compilação, então o resultado é apenas mais um literal — no pool. z envolve um new em tempo de execução, o compilador não pode dobrá-lo, e o objeto resultante vive fora do pool.

O método intern()

String#intern() faz duas coisas em uma única chamada:

  • Se uma string com os mesmos caracteres já estiver no pool, retorna essa referência do pool.
  • Caso contrário, adiciona esta string ao pool e a retorna.

O segundo comportamento é útil quando você constrói strings em tempo de execução a partir de um vocabulário pequeno, mas de alta frequência — nomes de cabeçalhos HTTP sendo analisados a partir de bytes, tokens de um lexer, nomes de colunas sendo lidos de um driver de banco de dados. Internalizá-las colapsa N objetos separados em um e significa que comparações posteriores podem usar == se você tiver medido que vale a pena o esforço.

String s1 = new String("status").intern();
String s2 = new String("status").intern();
System.out.println(s1 == s2);   // true — both refer to the pooled "status"

O problema: cada chamada a intern() custa uma pesquisa em hash, e as strings do pool vivem em uma tabela hash de tamanho fixo que não diminui. Se você internalizar entradas ilimitadas (consultas de busca digitadas pelo usuário, IDs de requisição), você preenche lentamente o pool com strings que nunca serão reutilizadas — um vazamento de memória em câmera lenta. Internalize apenas quando (a) o conjunto de valores for limitado e (b) você tiver medido um problema que vale a pena resolver.

Internos do pool (de forma superficial)

O pool é implementado como uma tabela hash dentro da JVM. No HotSpot, é uma StringTable com uma capacidade padrão que foi ajustada para cima ao longo dos anos (atualmente 65.536 buckets na maioria das builds). Você pode inspecioná-la na linha de comando:

java -XX:+PrintStringTableStatistics MyApp

Para código de aplicação, a implementação é invisível: você não pode perguntar "esta string está no pool?" via a API pública, e não precisa. O comportamento visível é == em literais iguais e intern() para optar por incluir strings computadas.

Por que == ainda é errado para strings

O pool pode fazer == parecer funcionar em entradas de teste:

String a = "hello";
String b = "hello";
if (a == b) { ... }   // happens to be true

Então alguém passa a string por BufferedReader.readLine() e == silenciosamente retorna falso. O contrato que você quer é "estes têm os mesmos caracteres?", e esse contrato é escrito como a.equals(b). O pool é uma otimização de memória, não uma estratégia de comparação — nunca dependa dele para correção.

Um exemplo trabalhado

O exemplo abaixo torna o comportamento do pool visível. Cada chamada a printRef mostra o hash de identidade do sistema (um substituto de uma linha para "qual objeto é este?") para que você possa ver onde os literais compartilham armazenamento e onde as strings computadas não compartilham.

java— editable, runs on the server

Leia primeiro os hashes de identidade: os literais e o dobramento em tempo de compilação compartilham um. runtimeConcat e fresh têm cada um o seu próprio. interned corresponde ao literal novamente, porque intern() retornou a instância do pool, não a alocada com new. Os resultados de == seguem diretamente das identidades; equals retorna true para todos eles porque, em termos de conteúdo, eles realmente são iguais.

O que vem a seguir

O pool existe porque String é imutável — compartilhar o mesmo objeto entre chamadores só é seguro se ninguém puder alterar seu conteúdo. O próximo capítulo aprofunda esse ponto: por que a imutabilidade foi escolhida, o que ela traz e o compromisso de design que ela impõe. Continue para imutabilidade de String em Java.

Prática

Prática
Duas variáveis `String`, `a` e `b`, foram ambas atribuídas com o mesmo literal `'hi'` em algum lugar do programa. Uma terceira, `c`, foi criada com `new String('hi')`. Qual resultado é garantido?
Duas variáveis `String`, `a` e `b`, foram ambas atribuídas com o mesmo literal `'hi'` em algum lugar do programa. Uma terceira, `c`, foi criada com `new String('hi')`. Qual resultado é garantido?
Was this page helpful?