Quem estudou um pouco sobre Java sabe que Strings possuem algumas peculiaridades. Provavelmente o leitor já sabe que elas são imutáveis, já ouviu falar do pool de Strings e que deve-se usar o método equals() ao invés do operador == para comparar o conteúdo de variáveis.

Neste artigo quero ir um pouco mais além, entendendo como isso funciona internamente.

Brincando com o == e com o pool de Strings

O Java utiliza um mecanismo chamado String interning, colocando as Strings num pool para tentar armazenar apenas uma cópia de cada sequência de caracteres em memória. Em tese, o programa usaria mesmo memória e seria mais eficiente em decorrência dessa otimização.

Quando o Java encontra literais String no código, ele retorna sempre uma mesma instância de String, que aponta para uma entrada no pool interno da JVM. Sendo assim, é bem possível usar o operador == para comparar duas variáveis que recebem literais String:

String literal = "str";
String outraLiteral = "str";

System.out.println(literal == outraLiteral); //exibe true

Inclusive, como o Java trata literais String como instâncias é possível comparar um literal diretamente, assim:

System.out.println(literal == "str"); //também exibe true

Por outro lado, não podemos confiar no operador de comparação quando não sabemos como a String foi criada, já que é possível criar outras instâncias de várias formas. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia); //exibe false

O código acima cria uma nova instância de String, que não é a mesma retornada pela JVM para o literal "str".

Mas, contudo, entretanto, isso não quer dizer que temos duas entradas de "str" no pool do Java. Como podemos verificar isso? Usando o método String.intern(), que retorna uma referência para a String que está no pool. Exemplo:

String novaInstancia = new String("str");
System.out.println("str" == novaInstancia.intern()); //exibe true

Outro exemplo:

String str1 = "teste";
String str2 = "outro teste".substring(6);
System.out.println(str1 == str2.intern()); //exibe true

Tudo muito interessante. Mas, e se criássemos uma String de uma forma mirabolante?

StringBuilder sb = new StringBuilder();
sb.append('s');
sb.append('t');
sb.append('r');
System.out.println("str" == sb.toString().intern()); //continua sendo true

Até aqui aprendemos que uma instância da classe String não representa diretamente o seu conteúdo, isto é, o conjunto de caracteres. Várias instâncias de String podem coexistir com o mesmo texto. A questão é que todas apontam para a mesma entrada no pool.

Continue lendo, pois ainda não esgotamos este assunto!

Mas então pare que serve o equals()?

Com as informações do tópico anterior poderíamos chegar precipitadamente à conclusão de que é sempre melhor comparar duas Strings usando o operador == e o método intern().

O método equals() da classe String compara todos os caracteres de duas Strings para verificar a igualdade, enquanto o == apenas verifica se as duas Strings apontam para a mesma entrada do pool, uma comparação numérica infinitamente mais eficiente do ponto de vista computacional.

Já que a comparação com == é muito mais rápida do que com o método equals(), devemos abandonar o equals() e usar o intern() em todo lugar? A resposta é não.

A verdade é que nem todas as Strings são internalizadas no pool imediatamente. Quando chamamos o método intern(), se ela não estiver lá, então o Java irá acrescentá-la.

O problema é que, uma vez no pool, a String vai para a memória permanente e não será mais coletada pelo garbage collector.

Quando se quer velocidade e o conjunto de valores é relativamente pequeno, usar o método intern() pode ser vantajoso. Mas se usarmos este recurso, por exemplo, para processamento de arquivos texto, XML, bancos de dados, logo esbarraremos num OutOfMemoryError.

Além disso, adicionar uma Strings no pool também pode ser uma operação “cara”. Além de ser necessário verificar se a String já existe lá (envolve o método hashCode() e modificação de um mapa), o Java provavelmente terá que tratar acessos concorrentes (mais de uma thread pode inserir elementos no pool).

Finalmente, uma grande desvantagem é o código ficar mais propenso a bugs (error prone), já que é preciso que o desenvolvedor sempre coloque o intern() quando necessário.

Concluindo, o conhecimento sobre o pool ajuda em casos específicos para otimização “fina” do código, mas o uso deve ser moderado.

Outras formas de comparação

Indo um pouco além da comparação exata de Strings, temos outras formas interessantes de comparação:

Case insensitive (sem considerar maiúsculas e minúsculas)

System.out.println("STR".equalsIgnoreCase("str")); //retorna true

Uma string contida em outra

System.out.println("###STR###".contains("STR")); //retorna true

Qual string é “maior” que a outra?

System.out.println("str1".compareTo("str2")); //retorna -1, pois "str1" é menor que "str2"

Ou:

System.out.println("str1".compareToIgnoreCase("STR2")); //retorna -1, ignorando a capitalização

O método compareTo retorna:

  • 1 se a primeira String for maior que a segunda
  • 0 se forem iguais
  • -1 se a primeira String for menor que a segunda

Começa com…

System.out.println("str1".startsWith("str")); //returna true, pois "str1" começa com "str"

Termina com…

System.out.println("str1".endsWith("r1")); //return true, pois "str1" termina com "r1"

Expressão regular

System.out.println("str2".matches("\\w{3}\\d")); //return true, pois corresponde à expressão regular

Está vazia?

String str1 = "";
System.out.println(str1.isEmpty());
System.out.println(str1.length() == 0);
System.out.println(str1.equals(""));

Particularmente eu prefiro o primeiro método para Java >= 6 e o segundo para as versões anteriores.


Este artigo foi baseado na minha resposta no StackOverflow em Português!