28 de jun. de 2010

Strings no Java


Intro

Salve,

Após conversar com colegas e tal, resolvi compartilhar o conhecimento sobre Strings e sua implementação e características na linguagem Java. Alguns detalhes inclusive fazem parte do exame para Programador Java Certificado (SCJP) e a utilização em outras linguagens é semelhante, como no C#, ActionScript, etc.

Espero que seja de bom proveito.


Strings

As Strings diferentemente dos tipos primitivos (int, long, float, double, char, double) são reprentadas por uma classe. Logo, toda declaração de String é uma instância. 

Ver exemplo:

import static java.lang.System.out;

public class Teste {
public static void main(String[] args) {
out.println("Teste");
}
}

No exemplo, a simples declaração de "Teste" gera uma instância (automaticamente) da classe String. Isto permite que eu faça código desta maneira:

String texto = "Teste";
out.println(texto.toUpperCase());
out.println("Teste".toUpperCase());

Ambas instruções de saída geram: TESTE

Como qualquer instância de uma classe, sendo um objeto, as String tem atributos e métodos. Mas existem duas características que fazem o tratamento de String um pouco diferente:

- Strings são imutáveis
- Strings literais são armazenadas no pool no espaço de geração permanente (PermGen Space).




Strings são imutáveis

Cada operação chamada em uma instância de String cria outra String (nova instância). Isto garante a integridade das referências. Ver exemplo:

String texto = "teste";
texto.toUpperCase()
out.println(texto);

Produz a saída: "teste".

Ao usar o método toUpperCase na variável texto, foi criado uma nova String em maiúscula. A String referenciada pela variável texto ainda é "teste" e, enquanto a variável texto não for reatribuida, sempre será. Se quiser que a variável texto aponte para o novo objeto String em maiúsculo faça o seguinte:

String texto = "teste";
texto = texto.toUpperCase()
out.println(texto);

É importante entender que cada alteração na String gera uma nova instância, e isto implica em questões de performance e confiabilidade. Exemplo:

String frase = "texto";
frase = frase + " de ";
frase = frase + "exemplo";

Neste caso, eu criei pelo menos 5 instâncias: "texto", " de ", "texto de", "exemplo", "texto de exemplo".

Este é um dos motivos por que é preferível usar StringBuilder quando é necessário concatenar muitas Strings. Exemplo:

StringBuilder frase = new StringBuilder();
frase.append("texto");
frase.append(" de ");
frase.append("exemplo");

Note que ainda assim, cada declaração literal gera uma instância, mas o StringBuilder (StringBuffer se for necessário sincronização para Threads) pode receber variáveis por exemplo.

[ :( ] Por exemplo, nunca faça isso:

String nomes = "";
for (String nome : dataSource.getNomes()) {
nomes = nomes + "; " + nome;
}

Supondo que seja uma lista de 100 nomes. A cada iteração é gerado uma nova String com o nomes + ";", e depois outra com nomes (já com o ;) mais o nome.

[ :) ] Faça assim:

StringBuilder nomes = new StringBuilder();
for (String nome : dataSource.getNomes()) {
nomes.append(nome);
}

Para pegar a String gerada faça: nomes.toString();


[ :( ] Como não há garantia de estar referenciando a mesma instância nunca faça isto:

if (texto == "sim") { ...

[ :| ] Talvez faça isto:

if (texto.equals("sim")) { ...

[ :) ] Mas, prefira fazer assim:

if ("sim".equals(texto)) { ...

Por que? Por que se texto vier nulo, no caso de texto.equals("sim") receberemos um belo NullPointerException. No caso de "sim".equals(texto), se a variável for nulo simplesmente avaliará falso. Estude o contrato do equals: http://java.sun.com/javase/6/docs/api/java/lang/Object.html#equals(java.lang.Object)


Entendido?




Strings são armazenadas no espaço de geração permanentente

O Java reutiliza Strings utilizando o pool de String, que é guardado no espaço de geração permanente, o PermGen Space.

Em outro artigo já falei sobre como a memória é gerenciada no Java, mas para clarear, existem 3 espaços básicos:
- A pilha (Stack): onde ficam as variáveis e os métodos;
- O Heap: onde ficam as instâncias de objetos;
- O espaço de geração permanente (PermGen): onde ficam armazenadas informações até o fim da aplicação, como as Classes e as Strings.

Ou seja, se eu declarar uma String "teste", assim, literalmente na aplicação, ela irá para o PermGen.

Acredito que os leitores saibam que o operador == compara igualdade de referências, não de conteúdos, certo? Bem, vamos a um exemplo:

String a = "teste";
String b = "teste";
out.println(a == b);

Qual é a saída?

true

Embora pareça óbvio, nem sempre é. Neste caso, acusou verdadeiro por que ambas variáveis apontam para a mesma instância, já que o primeiro "teste" foi colocado no pool e reutilizado para a variável b, então NESTE CASO 'a' e 'b' referenciam o mesmo objeto. 

O fato de as String serem imutáveis favorecem esta técnica, já que a instância "teste" no PermGen nunca será alterada.


Bem, eu posso forçar a criação de uma nova instância, em outras palavras, não aproveitar do pool, fazendo assim:

String a = "teste";
String b = new String("teste");
out.println(a == b); 

Ambas valem "teste", mas a saída da operação é false. Sempre que eu utilizo a palavra-chave new com Strings, eu estou ignorando o pool.

[ :( ] Evite usar o new para Strings, por exemplo, a instrução new String("teste") gera duas instâncias, a literal "teste" vai para o PermGen, e outra instância é gerada no Heap devido a palavra-chave new. 

[ :) ] Aproveite o pool e só instancie diretamente quando tiver uma razão clara para isto.



Operações com Strings

A classe String já tem inumeras operações, desde conversão de caixa até extrações (substrings). 

Contudo, algumas operações podem lhe fazer falta de alguma maneira.

Inicialmente, é normal pensarmos em estender a classe String, só que isto não é possível.

Devido ao contrato de imutabilidade, a classe String é final (sealed), e não pode ser estendida.

Por isso é comum criarmos métodos auxiliares em outra classe, tipo, uma StringUtil (no java temos muito destas xxxUtil).

Por exemplo, se preciso de um método que me diga se a String é um número posso fazer um método nesta classe utilitária. Fiz um exemplo tosco abaixo:


import static java.lang.System.out;
import java.util.regex.*;

public class Teste {
public static void main(String[] args) {
String b = new String("2132");
out.println(StringUtil.isNumber(b));
}

}

class StringUtil {
public static boolean isNumber(String s) {
if (s == null) return false;
Pattern p = Pattern.compile("\\d*");
Matcher m = p.matcher(s);
return m.matches();
}
}


Do meu ponto de vista este é um código procedural, não que seja ruim antes que me atirem pedras, mas que não é bem a proposta do paradigma OO. 

[ ! ] Não necessariamente por utilizar uma linguagem orientada a objetos estamos desenvolvendo um programa orientado a objetos, entende?

Bem, isto fica para outro tópico.


Dúvidas, comentários, críticas, sugestões, etc, fiquem a vontade.


Um abraço

Márcio Torres