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 segunda0
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!