Java StringBuilder
Uma String do Java nunca muda depois de criada. Cada texto = texto + "x" cria uma String nova, copiando tudo o que já existia. Num laço com milhares de voltas, isso fica lento de verdade. O StringBuilder ("construtor de strings") é um texto que pode mudar: você vai acrescentando pedaços e, no fim, pega a String pronta.
- O que quer dizer "a
Stringé imutável" e o que acontece no+dentro de um laço. - Medir a diferença de velocidade entre
+eStringBuilder. - Os métodos do
StringBuilder:append,insert,reverse,deleteCharAt,delete,replace,setLength,toString. - Encadear chamadas (
sb.append(a).append(b)). - A diferença entre
StringBuildereStringBuffer, e quando o+comum é a melhor escolha.
A ideia antes do código
Imagine que a Ana escreve uma lista de compras a caneta, num cartão. Para acrescentar "leite", ela não pode escrever no cartão antigo (não há espaço e caneta não apaga): pega um cartão novo, copia tudo e escreve "leite" no fim. Para acrescentar "pão", outro cartão novo, copia tudo de novo… Com 3 itens, tudo bem. Com 10 mil, ela passa o dia copiando.
Agora imagine um caderno a lápis: ela só escreve o item novo no fim da página. Pode apagar uma letra, inserir uma palavra no meio. Quando a lista fica pronta, ela passa a limpo uma vez.
A String é o cartão a caneta. O StringBuilder é o caderno a lápis, e o toString() é "passar a limpo".
A String não muda
Você viu em Métodos de String que métodos como toUpperCase() devolvem uma String nova. Veja o que acontece quando o resultado não é guardado, e compare com um StringBuilder:
public class Main {
public static void main(String[] args) {
String nome = "Ana";
nome.toUpperCase();
System.out.println(nome);
nome = nome.toUpperCase();
System.out.println(nome);
StringBuilder sb = new StringBuilder("Ana");
sb.reverse();
System.out.println(sb);
}
}
$ java Main.java Ana ANA anA
A linha 4 criou "ANA" e jogou fora: nome continuou "Ana". Só na linha 6, guardando o resultado na variável, nome passou a apontar para o texto novo. Já o sb.reverse() mudou o próprio objeto: não foi preciso guardar nada. Dizemos que a String é imutável e o StringBuilder é mutável.
O + no laço: medindo
Vamos montar um texto com n letras "x", primeiro com String e +, depois com StringBuilder, e cronometrar com System.nanoTime() (um relógio em nanossegundos; dividindo por 1.000.000 temos milissegundos, ms):
public class Main {
static long comString(int n) {
long inicio = System.nanoTime();
String texto = "";
for (int i = 0; i < n; i++) {
texto = texto + "x";
}
return (System.nanoTime() - inicio) / 1_000_000;
}
static long comStringBuilder(int n) {
long inicio = System.nanoTime();
StringBuilder texto = new StringBuilder();
for (int i = 0; i < n; i++) {
texto.append("x");
}
String resultado = texto.toString();
return (System.nanoTime() - inicio) / 1_000_000;
}
public static void main(String[] args) {
int[] tamanhos = {10_000, 20_000, 40_000, 80_000};
for (int n : tamanhos) {
System.out.println(n + " letras: String " + comString(n)
+ " ms, StringBuilder " + comStringBuilder(n) + " ms");
}
}
}
$ javac Main.java $ java Main 10000 letras: String 43 ms, StringBuilder 1 ms 20000 letras: String 58 ms, StringBuilder 0 ms 40000 letras: String 170 ms, StringBuilder 1 ms 80000 letras: String 618 ms, StringBuilder 1 ms
Os números mudam de computador para computador (e de uma execução para outra), mas o padrão é sempre o mesmo:
- Com
StringBuilder, o tempo é praticamente zero em todos os tamanhos. - Com
+, cada vez que o tamanho dobra, o tempo fica perto de quatro vezes maior (170 → 618 ms). Na volta 40.000, o Java copia 40.000 letras para acrescentar uma; somando todas as voltas, são centenas de milhões de cópias. Com 1 milhão de letras, esse programa levaria minutos.
+ numa linha só?
Uma expressão como "Olá, " + nome + "! Você tem " + idade + " anos." é perfeitamente boa: o compilador já a transforma numa montagem eficiente. O problema é só o + que se repete dentro de um laço, acumulando na mesma variável. Para juntar texto numa linha, continue usando + (veja Concatenação) ou String.format.
Usando o StringBuilder
- StringBuilder sb = new StringBuilder();
- Cria um "caderno" vazio. Também dá para começar com um texto:
new StringBuilder("Ana"). Fica emjava.lang: semimport. - sb.append(x)
- "Acrescentar": coloca
xno fim. Aceita texto, números,char,boolean, objetos. - sb.toString()
- Devolve uma
Stringcom o conteúdo atual. Oprintln(sb)chama otoString()sozinho.
Os outros métodos, um por um, no JShell (acompanhe o texto mudando a cada linha):
$ jshell
| Welcome to JShell -- Version 25.0.4.1
| For an introduction type: /help intro
jshell> StringBuilder sb = new StringBuilder();
sb ==>
jshell> sb.append("Ana")
$2 ==> Ana
jshell> sb.append(": ").append(9.5)
$3 ==> Ana: 9.5
jshell> sb.insert(0, "Aluna ")
$4 ==> Aluna Ana: 9.5
jshell> sb.length()
$5 ==> 14
jshell> sb.charAt(0)
$6 ==> 'A'
jshell> sb.indexOf(":")
$7 ==> 9
jshell> sb.replace(6, 9, "Carla")
$8 ==> Aluna Carla: 9.5
jshell> sb.deleteCharAt(sb.length() - 1)
$9 ==> Aluna Carla: 9.
jshell> sb.setCharAt(0, 'a')
jshell> sb
sb ==> aluna Carla: 9.
jshell> sb.delete(0, 6)
$12 ==> Carla: 9.
jshell> String texto = sb.toString();
texto ==> "Carla: 9."
jshell> new StringBuilder("roma").reverse()
$14 ==> amor
jshell> /exit
| Goodbye
| Linha | O que aconteceu |
|---|---|
append("Ana") | Acrescentou no fim. O JShell mostra o resultado porque o append devolve o próprio sb |
append(": ").append(9.5) | Encadeamento: como cada append devolve o sb, dá para chamar o próximo em seguida. O 9.5 (um double) virou texto |
insert(0, "Aluna ") | Inseriu na posição 0 (o começo). As posições contam a partir de 0, como na String |
length(), charAt(0), indexOf(":") | Iguais aos da String: tamanho, caractere numa posição, onde está um trecho |
replace(6, 9, "Carla") | Trocou o trecho das posições 6 a 8 (Ana) por Carla. O fim (9) não entra |
deleteCharAt(sb.length() - 1) | Apagou o último caractere (o 5) |
setCharAt(0, 'a') | Trocou um caractere. Não devolve nada (void), por isso o JShell não mostrou resultado |
delete(0, 6) | Apagou as posições 0 a 5 (aluna ) |
toString() | "Passou a limpo": uma String comum (repare nas aspas na resposta do JShell) |
reverse() | Inverteu o texto |
Missão: o programa monta a contagem regressiva com + num laço. Reescreva usando um StringBuilder e append. A saída deve continuar sendo 5 4 3 2 1 Fogo!.
public class Main {
public static void main(String[] args) {
String contagem = "";
for (int i = 5; i >= 1; i--) {
contagem = contagem + i + " ";
}
contagem = contagem + "Fogo!";
System.out.println(contagem);
}
}
public class Main {
public static void main(String[] args) {
StringBuilder contagem = new StringBuilder();
for (int i = 5; i >= 1; i--) {
contagem.append(i).append(' ');
}
contagem.append("Fogo!");
System.out.println(contagem);
}
}
Um exemplo completo: o relatório da turma
A Ana quer montar um relatório com uma linha por aluno e, no fim, a lista dos nomes separados por vírgula. O truque clássico da vírgula: acrescentar ", " depois de cada nome e, no fim, cortar os dois últimos caracteres com setLength ("definir o tamanho").
public class Main {
public static void main(String[] args) {
String[] alunos = {"Ana", "Bruno", "Carla"};
double[] notas = {9.5, 7.0, 8.5};
StringBuilder relatorio = new StringBuilder();
relatorio.append("Notas da turma\n");
for (int i = 0; i < alunos.length; i++) {
relatorio.append("- ").append(alunos[i]).append(": ").append(notas[i]).append('\n');
}
StringBuilder nomes = new StringBuilder();
for (String a : alunos) {
nomes.append(a).append(", ");
}
nomes.setLength(nomes.length() - 2);
relatorio.append("Alunos: ").append(nomes);
System.out.println(relatorio);
}
}
$ java Main.java Notas da turma - Ana: 9.5 - Bruno: 7.0 - Carla: 8.5 Alunos: Ana, Bruno, Carla
- append('\n')
- Um
charde quebra de linha (veja Caracteres Especiais). Cada aluno ganha a sua linha. - nomes.setLength(nomes.length() - 2)
- Antes,
nomesera"Ana, Bruno, Carla, ". Encurtar o tamanho em 2 corta o", "que sobrou no fim. - relatorio.append("Alunos: ").append(nomes)
- Um
StringBuilderpode receber outro.
Para juntar itens com separador, o Java já tem String.join(", ", alunos), que dá Ana, Bruno, Carla sem o truque da vírgula. Para repetir um texto, "x".repeat(80000). Use o StringBuilder quando a montagem tem lógica no meio (ifs, cálculos, formatos diferentes por linha).
StringBuilder × StringBuffer
Você vai encontrar, em código antigo e em livros, o StringBuffer. Ele tem os mesmos métodos (append, insert, reverse…). A diferença é interna:
StringBuilder | StringBuffer | |
|---|---|---|
| Desde | Java 5 (2004) | Java 1.0 (1996) |
| Seguro para várias threads ao mesmo tempo | Não | Sim (métodos synchronized) |
| Velocidade | Mais rápido | Um pouco mais lento (paga pela sincronização em toda chamada) |
| Quando usar | Quase sempre: um texto montado dentro de um método | Raramente: quando várias threads escrevem no mesmo objeto (e mesmo aí há opções melhores) |
Resumo prático: use StringBuilder. O StringBuffer existe por compatibilidade com código antigo; ao ver um, saiba que ele funciona igual.
Qual usar: +, StringBuilder ou outro
| Situação | Use |
|---|---|
| Juntar algumas partes numa expressão | + ou String.format |
| Acumular texto dentro de um laço | StringBuilder |
| Inserir, apagar, inverter, trocar caracteres | StringBuilder |
| Juntar uma lista com separador | String.join |
| Repetir um texto | "…".repeat(n) |
| Várias threads escrevendo no mesmo texto | StringBuffer |
Quando dá errado
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Ana");
String nome = sb;
System.out.println(nome);
}
}
$ java Main.java
Main.java:4: error: incompatible types: StringBuilder cannot be converted to String
String nome = sb;
^
1 error
error: compilation failed
O que aconteceu: StringBuilder e String são classes diferentes. O println aceita os dois, mas uma variável String só aceita String.
Como resolver: String nome = sb.toString();.
StringBuilder com o mesmo texto não são equals
public class Main {
public static void main(String[] args) {
StringBuilder a = new StringBuilder("uva");
StringBuilder b = new StringBuilder("uva");
System.out.println(a.equals(b));
System.out.println(a.toString().equals(b.toString()));
System.out.println(a.compareTo(b) == 0);
}
}
$ java Main.java false true true
O que aconteceu: o StringBuilder não redefine o equals (veja A Classe Object): para ele, equals é o mesmo que ==, "é o mesmo objeto?". Como é um texto que muda, comparar conteúdos não faz muito sentido.
Como resolver: compare os textos prontos (a.toString().equals(b.toString())) ou use a.compareTo(b) == 0.
StringIndexOutOfBoundsException ao apagar
public class Main {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("uva");
sb.deleteCharAt(sb.length());
}
}
$ java Main.java Exception in thread "main" java.lang.StringIndexOutOfBoundsException: Index 3 out of bounds for length 3 at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:55) at java.base/jdk.internal.util.Preconditions$1.apply(Preconditions.java:52) at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:213) at java.base/jdk.internal.util.Preconditions$4.apply(Preconditions.java:210) at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:98) at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:106) at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:302) at java.base/java.lang.String.checkIndex(String.java:4904) at java.base/java.lang.AbstractStringBuilder.deleteCharAt(AbstractStringBuilder.java:1081) at java.base/java.lang.StringBuilder.deleteCharAt(StringBuilder.java:298) at Main.main(Main.java:4)
O que aconteceu: "uva" tem tamanho 3 e posições 0, 1 e 2. A posição 3 não existe. O último caractere é sempre length() - 1.
Como resolver: sb.deleteCharAt(sb.length() - 1), e só se o texto não estiver vazio (if (sb.length() > 0)).
Referência
| Método | O que faz | Devolve |
|---|---|---|
new StringBuilder() / new StringBuilder("txt") | Cria vazio / com texto inicial | |
append(x) | Acrescenta no fim (qualquer tipo) | o próprio sb |
insert(pos, x) | Insere na posição | o próprio sb |
delete(ini, fim) | Apaga de ini até fim - 1 | o próprio sb |
deleteCharAt(pos) | Apaga um caractere | o próprio sb |
replace(ini, fim, "txt") | Troca um trecho | o próprio sb |
reverse() | Inverte | o próprio sb |
setCharAt(pos, c) | Troca um caractere | nada (void) |
setLength(n) | Corta (ou completa) até o tamanho n; setLength(0) esvazia | nada |
length(), charAt(pos), indexOf("txt") | Como na String | int, char, int |
toString() | O texto pronto | String |
Exercícios
1. Acrescentar
Complete para acrescentar "!" no fim:
2. Passar a limpo
Complete para obter a String:
3. Inverter
O que new StringBuilder("Ana").reverse() produz?
4. Por que texto = texto + "x" dentro de um laço grande é lento?
5. Qual a principal diferença entre StringBuilder e StringBuffer?
6. StringBuilder sb = new StringBuilder("abc"); sb.deleteCharAt(0); Quanto vale sb?
Missão final: um palíndromo é uma palavra que se lê igual de trás para frente. Complete o laço: inverta cada palavra com new StringBuilder(p).reverse().toString() e compare com a original ignorando maiúsculas (equalsIgnoreCase). Mostre arara é palíndromo, banana não é palíndromo (ananab) e Reviver é palíndromo.
public class Main {
public static void main(String[] args) {
String[] palavras = {"arara", "banana", "Reviver"};
for (String p : palavras) {
String invertida = "";
// inverta p e compare
}
}
}
public class Main {
public static void main(String[] args) {
String[] palavras = {"arara", "banana", "Reviver"};
for (String p : palavras) {
String invertida = new StringBuilder(p).reverse().toString();
if (p.equalsIgnoreCase(invertida)) {
System.out.println(p + " é palíndromo");
} else {
System.out.println(p + " não é palíndromo (" + invertida + ")");
}
}
}
}
Resumo
Stringé imutável: todo "alterar" cria umaStringnova.StringBuilderé mutável: os métodos mudam o próprio objeto.+acumulando texto num laço é lento (dobrar o tamanho quadruplica o tempo). UseStringBuildereappend;+numa expressão só continua ótimo.- Métodos:
append,insert,delete,deleteCharAt,replace,reverse,setCharAt,setLength,toString. A maioria devolve o própriosb, o que permite encadear. - Para virar
String:toString(). Para comparar: compare ostoString()(oequalsdoStringBuildernão compara texto). StringBufferé a versão antiga e sincronizada; prefiraStringBuilder.