Java StringBuilder
Costruisci stringhe mutabili in modo efficiente in Java con StringBuilder — append, insert, reverse e molto altro.
Una String è immutabile; crescerla con += in un ciclo ha costo quadratico. StringBuilder è la risposta della libreria standard: un singolo oggetto con un buffer interno ridimensionabile che puoi accodare, inserire, eliminare e invertire in-place, per poi convertirlo in una String immutabile una sola volta alla fine. È il pilastro di ogni moderno pattern di costruzione di stringhe nel JDK, e la scelta giusta nel momento in cui ti ritrovi ad accumulare testo in un ciclo o attraverso più chiamate a metodi.
Costruire un builder
StringBuilder sb = new StringBuilder(); // empty, capacity 16
StringBuilder withCap = new StringBuilder(1024); // empty, preallocated capacity
StringBuilder fromText = new StringBuilder("hi "); // capacity = length + 16Il costruttore senza argomenti parte con capacità 16, che va bene per risultati brevi ma è una scelta pessimistica per quelli lunghi. Se sai approssimativamente quanto sarà grande la stringa finale, passa la capacità subito — ogni grow evitato è un'allocazione di array in meno e una arraycopy in meno. new StringBuilder(estimatedLength) è la singola micro-ottimizzazione più efficace in questa parte del libro.
Concatenazione a catena: ogni metodo mutante restituisce this
Ogni metodo che modifica StringBuilder restituisce il builder stesso, quindi le chiamate si compongono in una singola espressione:
String greeting = new StringBuilder()
.append("Hello, ")
.append(name)
.append('!')
.append('\n')
.toString();Questa è convenzione, non magia; potresti spezzarla in più istruzioni separate senza alcuna differenza funzionale. Lo stile concatenato rispecchia il modo in cui il compilatore riscrive le catene di + internamente, che è proprio il motivo per cui quella forma risulta naturale da leggere in Java.
La famiglia dei metodi mutanti
StringBuilder ha un'interfaccia piccola e mirata:
append— aggiunge testo o qualsiasi primitivo alla fine. Con overload per ogni primitivo,char[],CharSequenceeObject(chiamatoString).insert(offset, ...)— stessi overload, ma in una posizione arbitraria.delete(start, end),deleteCharAt(i)— rimuove un intervallo o un singolo carattere.replace(start, end, replacement)— sostituisce un intervallo con una nuova sottostringa; le lunghezze possono differire.reverse()— inverte il buffer in-place.setCharAt(i, ch)— riscrive un singolo carattere.setLength(n)— tronca (o riempie con) fino ancaratteri.
Questi metodi modificano il buffer; non restituiscono una nuova String. Per ottenere uno snapshot del contenuto come stringa immutabile, chiama toString().
Ispezione e conversione
length()— conteggio attuale dei caratteri.capacity()— dimensione corrente dell'array interno. Sempre ≥length().charAt(i),substring(start)/substring(start, end)— accesso in lettura, identico aString.indexOf(s),lastIndexOf(s)— individua una sottostringa.toString()— produce unaStringimmutabile. Chiamalo una volta sola alla fine; chiamarlo ripetutamente durante la costruzione è allocazione sprecata.ensureCapacity(n)— pre-espande il buffer ad almenon.trimToSize()— riduce il buffer per adattarsi al contenuto corrente. Raramente necessario.
Come cresce il buffer
Internamente, StringBuilder contiene un byte[] (o char[] sui JDK più vecchi). Quando un append causerebbe un overflow, il buffer viene riallocato a circa 2 × oldCapacity + 2, e il vecchio contenuto viene copiato. Ogni crescita è O(n) rispetto alla dimensione corrente, ma il pattern di raddoppio rende il costo totale di n append O(n) ammortizzato — ben diverso dalla concatenazione ripetuta di String, dove lo stesso ciclo è O(n²).
append "a" — capacity 16, length 1
... 15 more — capacity 16, length 16
append "b" — grow to 34, length 17
... 17 more — capacity 34, length 34
append "c" — grow to 70, length 35Se conosci la lunghezza finale, eviti tutte quelle riallocazioni costruendo il builder con quella capacità direttamente.
StringBuilder vs l'operatore +
Per l'assemblaggio di stringhe breve e staticamente noto, il compilatore fa la cosa giusta da solo. "Hello, " + name + "!" viene riscritto a tempo di compilazione in una singola catena StringBuilder o in una chiamata a StringConcatFactory.makeConcatWithConstants (Java 9+). Entrambe sono efficienti. Non hai bisogno di gestire manualmente queste espressioni.
Il pattern da evitare è += all'interno di un ciclo con un numero sconosciuto di iterazioni:
// O(n²) — every += allocates a new String holding everything seen so far
String out = "";
for (String token : tokens) {
out += token + "|";
}
// O(n) — one buffer, one final String
StringBuilder sb = new StringBuilder();
for (String token : tokens) {
sb.append(token).append('|');
}
String out = sb.toString();Se il ciclo esegue poche iterazioni, la differenza è invisibile. Con qualche migliaio di token, diventa un problema misurabile. Con un milione, la prima forma è un blocco e la seconda richiede millisecondi.
StringBuilder non è thread-safe
StringBuilder omette deliberatamente la sincronizzazione per essere veloce nel caso (di gran lunga più comune) a singolo thread. Se due thread accodano allo stesso builder contemporaneamente, i risultati sono indefiniti: scritture perse, caratteri sovrascritti o un ArrayIndexOutOfBoundsException dal percorso di crescita. Per il raro caso in cui un builder venga condiviso tra thread, usa il gemello sincronizzato — StringBuffer — al suo posto. In pratica non condividi quasi mai un builder; ogni thread costruisce il proprio.
Un esempio pratico
Il programma seguente usa ogni parte dell'interfaccia rilevante nel codice quotidiano: append concatenato, un insert esplicito, un replace che cambia la lunghezza di un intervallo, un reverse e un unico toString finale. La capacità viene stampata all'inizio e alla fine per rendere visibile il raddoppio del buffer.
Osserva i valori di capacità nell'output (capacity=32 all'inizio, capacity=66 alla fine). Ogni append della catena rientra nella capacità iniziale di 32 — la stringa costruita è di soli 24 caratteri. L'insert e il replace portano poi la lunghezza a 40, che supera 32 e scatena esattamente una crescita (a 2 × 32 + 2 = 66). Pre-dimensionare più vicino alla lunghezza finale reale — new StringBuilder(48) qui — avrebbe evitato quella singola riallocazione. Questo è tutto il gioco: più accurata è la stima della capacità, meno eventi di copia-in-crescita si verificano.
Cosa c'è dopo
StringBuilder è veloce perché non è thread-safe. Il suo gemello sincronizzato è la risposta giusta nel (raro) caso in cui si voglia davvero condividere un buffer tra thread — stessa API, metodi che acquisiscono un lock. Continua con Java StringBuffer.