StringBuffer append() Method in Java (With Practical Examples)

You notice it the moment your code hits production logs: a “simple” string concatenation inside a loop becomes a hot spot, your log lines look slightly wrong (missing separators, weird Unicode), and the quick fix you made last week is now hard to read. When I see that pattern, I reach for a mutable text accumulator—often StringBuilder, and sometimes StringBuffer when I truly need synchronized, thread-safe mutation.

StringBuffer.append() is the workhorse API on StringBuffer: it takes almost anything you can throw at it, turns it into text, and appends it to an internal character sequence. The power is in the overloads: Java gives you 13 distinct append(...) forms, which means you can write clean, allocation-friendly code without manual conversions.

I’ll walk you through how append() behaves, the key overloads you’ll use day-to-day, and the edge cases I see most often: null, array slices, Unicode code points, accidental sharing across threads, and performance traps. By the end, you should be able to pick the right overload by instinct and write examples you can paste into a main() method and run.

The mental model: one growing buffer, many appends

StringBuffer is a mutable sequence of characters. Think of it like a resizable notepad:

  • It has a length (how many characters are currently written).
  • It has a capacity (how many characters it can hold before it must grow).
  • append(...) writes more characters at the end and returns the same object, so you can chain calls.

Internally, a StringBuffer grows when needed (typically by allocating a larger internal array and copying existing data). That growth step is why “append inside a loop” is still something I profile and tune: if you repeatedly outgrow capacity, you create avoidable allocations and copies.

Thread safety is the other key trait: StringBuffer methods are synchronized, which makes single-instance mutation safe across threads, but also adds overhead. If you don’t need cross-thread mutation of the same instance, I prefer StringBuilder for lower synchronization cost.

Length vs capacity (and why I care)

I keep these two numbers separate in my head:

  • Length answers: “How many characters are already in the buffer?” (sb.length())
  • Capacity answers: “How many characters can I append before the buffer has to grow?” (sb.capacity())

When capacity is too small, Java grows the internal storage and copies the old characters over. That copy is usually fast, but if it happens repeatedly in a loop (especially inside hot code paths), it adds up.

Here’s a tiny runnable demo that shows the difference:

public class LengthCapacityDemo {

public static void main(String[] args) {

StringBuffer sb = new StringBuffer(4);

System.out.println("length=" + sb.length() + " capacity=" + sb.capacity());

sb.append("ab");

System.out.println("length=" + sb.length() + " capacity=" + sb.capacity() + " value=" + sb);

sb.append("cdefgh");

System.out.println("length=" + sb.length() + " capacity=" + sb.capacity() + " value=" + sb);

}

}

I also reach for ensureCapacity(int minimumCapacity) when I can estimate size up front. It’s not mandatory, but it’s one of the easiest “free” performance wins in string-building code.

The append() overloads you actually get

StringBuffer overloads append(...) for primitives and common text types. I group them like this (because it matches how I read code):

Category

Overload

What it appends —

— Primitives

append(boolean)

"true" or "false"

append(char)

that single UTF-16 char

append(int)

decimal string form

append(long)

decimal string form

append(float)

floating-point string form

append(double)

floating-point string form Arrays

append(char[])

entire array content

append(char[], int, int)

slice: offset, len Text-ish types

append(String)

the string (or "null")

append(StringBuffer)

content of another buffer

append(CharSequence)

content of any CharSequence

append(CharSequence, int, int)

subsequence slice Objects

append(Object)

String.valueOf(obj)

If you remember nothing else: there’s almost always an overload that matches your type directly—so you don’t need "" + value, String.valueOf(...) everywhere, or manual loops over characters.

One extra method worth knowing (not technically an append overload): appendCodePoint(int) appends a Unicode code point safely, even when it requires a surrogate pair.

Runnable example: chaining appends for structured text

I like to start with a realistic shape: building a log line, a cache key, or a small protocol message.

import java.time.Instant;

public class AppendChainingDemo {

public static void main(String[] args) {

StringBuffer line = new StringBuffer(128);

String requestId = "req-7f3a";

int status = 200;

long durationMs = 37L;

boolean cacheHit = true;

line.append("ts=")

.append(Instant.now())

.append(" requestId=")

.append(requestId)

.append(" status=")

.append(status)

.append(" durationMs=")

.append(durationMs)

.append(" cacheHit=")

.append(cacheHit);

System.out.println(line.toString());

}

}

Notes from experience:

  • I often pass an initial capacity when I have a rough upper bound. It reduces growth copies.
  • Chaining reads cleanly because append() returns this.
  • Appending an Instant uses append(Object) and calls toString() under the hood.

What the compiler does with + (and why append still matters)

One subtle point: Java can rewrite a single expression with + into something like a StringBuilder under the hood.

So code like this is usually fine:

String line = "status=" + status + " durationMs=" + durationMs;

But code like this is still a trap:

String out = "";

for (int i = 0; i < 1000; i++) {

out = out + i + ",";

}

That loop forces repeated allocations because String is immutable, so each iteration creates a new string. That’s the exact scenario where StringBuffer.append() (or more commonly StringBuilder.append()) shines.

Overload-by-overload examples (with the edge cases that matter)

Below I’ll cover each overload with a runnable snippet and the behavior I care about when reviewing code.

1) append(boolean a)

Appends "true" or "false".

public class AppendBooleanDemo {

public static void main(String[] args) {

StringBuffer sb = new StringBuffer("featureEnabled=");

sb.append(true);

System.out.println(sb);

sb = new StringBuffer("featureEnabled=");

sb.append(false);

System.out.println(sb);

}

}

2) append(char a)

Appends a single UTF-16 char. This is not the same as “a full Unicode character” for emoji and some symbols (we’ll fix that with appendCodePoint later).

public class AppendCharDemo {

public static void main(String[] args) {

StringBuffer sb = new StringBuffer("grade=");

sb.append(‘A‘);

System.out.println(sb);

sb = new StringBuffer("pathSeparator=");

sb.append(‘/‘);

System.out.println(sb);

}

}

3) append(int a)

Appends the decimal string form.

public class AppendIntDemo {

public static void main(String[] args) {

int port = 8080;

StringBuffer sb = new StringBuffer("port=");

sb.append(port);

System.out.println(sb);

}

}

#### A note on negative numbers and bases

append(int) always uses the default decimal representation (base 10). If you need hex, binary, or a specific base, convert first:

public class AppendIntBaseDemo {

public static void main(String[] args) {

int mask = 255;

StringBuffer sb = new StringBuffer();

sb.append("dec=").append(mask)

.append(" hex=").append(Integer.toHexString(mask))

.append(" bin=").append(Integer.toBinaryString(mask));

System.out.println(sb);

}

}

4) append(long a)

Same idea, just wider.

public class AppendLongDemo {

public static void main(String[] args) {

long userId = 9123456789012L;

StringBuffer sb = new StringBuffer("userId=");

sb.append(userId);

System.out.println(sb);

}

}

5) append(float a) and 6) append(double a)

Appends using Java’s standard floating-point string conversion. I watch for three issues:

  • You might get scientific notation for very large/small numbers.
  • Floating-point values can expose representation quirks (for example, 0.1 isn’t exactly representable in binary).
  • For user-facing output, you often want consistent formatting (fixed decimals, locale rules).

public class AppendFloatingPointDemo {

public static void main(String[] args) {

float cpuLoad = 0.42f;

double price = 19.99;

StringBuffer sb = new StringBuffer();

sb.append("cpuLoad=").append(cpuLoad)

.append(" price=").append(price);

System.out.println(sb);

}

}

If I’m writing logs, I’m usually okay with Java’s default conversion. If I’m writing invoices, reports, CSV exports, or anything humans read, I format explicitly (more on that later).

7) append(char[] astr)

Appends the whole array. This is great when you already have characters (for example, you read from a stream or decoded something into a buffer).

public class AppendCharArrayDemo {

public static void main(String[] args) {

char[] countryCode = new char[] {‘U‘,‘S‘,‘A‘};

StringBuffer sb = new StringBuffer("country=");

sb.append(countryCode);

System.out.println(sb);

}

}

8) append(char[] astr, int offset, int len)

This is the overload I use when I’m dealing with reusable buffers (parsers, tokenizers, I/O loops). The edge case is bounds: bad offset/len throws IndexOutOfBoundsException.

public class AppendCharArraySliceDemo {

public static void main(String[] args) {

char[] data = "token=abc123;expires=2026-12-01".toCharArray();

// Append only "abc123"

int start = "token=".length();

int length = "abc123".length();

StringBuffer sb = new StringBuffer("extracted=");

sb.append(data, start, length);

System.out.println(sb);

}

}

#### Practical scenario: building a string while reading a Reader

Here’s a very common pattern: you read text in chunks into a reusable char[] and append only the portion you just read.

import java.io.IOException;

import java.io.Reader;

import java.io.StringReader;

public class AppendFromReaderDemo {

public static void main(String[] args) throws IOException {

Reader r = new StringReader("Hello\nworld\nfrom a Reader\n");

char[] buf = new char[8];

StringBuffer sb = new StringBuffer();

int n;

while ((n = r.read(buf)) != -1) {

sb.append(buf, 0, n);

}

System.out.println(sb);

}

}

I like this example because it shows why the slice overload exists: it prevents appending stale content beyond the bytes/characters you actually read.

9) append(String str)

Two behaviors I always keep in mind:

  • Appends the string as-is.
  • If str is null, it appends the four characters "null" (not an empty string).

public class AppendStringDemo {

public static void main(String[] args) {

StringBuffer sb = new StringBuffer("name=");

sb.append("Amina");

System.out.println(sb);

String middleName = null;

sb = new StringBuffer("middleName=");

sb.append(middleName);

System.out.println(sb); // middleName=null

// If you want empty instead of "null", guard it yourself:

sb = new StringBuffer("middleName=");

sb.append(middleName == null ? "" : middleName);

System.out.println(sb); // middleName=

}

}

A cleaner pattern I often use is Objects.toString(value, "") when I want an empty fallback.

import java.util.Objects;

public class AppendNullFallbackDemo {

public static void main(String[] args) {

String middleName = null;

StringBuffer sb = new StringBuffer("middleName=");

sb.append(Objects.toString(middleName, ""));

System.out.println(sb);

}

}

10) append(StringBuffer sb)

Appends another StringBuffer’s content. This is handy when you build segments in helpers.

public class AppendStringBufferDemo {

public static void main(String[] args) {

StringBuffer prefix = new StringBuffer("env=prod ");

StringBuffer message = new StringBuffer("action=").append("deploy");

StringBuffer line = new StringBuffer();

line.append(prefix).append(message);

System.out.println(line);

}

}

#### Aliasing gotcha: appending a buffer doesn’t “link” them

One thing I remind people in reviews: append(otherBuffer) copies the characters that exist at that moment. It does not create a live link.

public class AppendBufferAliasingDemo {

public static void main(String[] args) {

StringBuffer a = new StringBuffer("A");

StringBuffer b = new StringBuffer("B");

a.append(b);

b.append("!");

System.out.println("a=" + a); // a=AB

System.out.println("b=" + b); // b=B!

}

}

11) append(CharSequence s)

CharSequence is a nice umbrella interface: String, StringBuilder, StringBuffer, and others (like java.nio.CharBuffer). If s is null, you’ll get "null" here too.

public class AppendCharSequenceDemo {

public static void main(String[] args) {

CharSequence region = "us-east";

StringBuilder service = new StringBuilder("billing");

StringBuffer sb = new StringBuffer();

sb.append("region=").append(region)

.append(" service=").append(service);

System.out.println(sb);

}

}

12) append(CharSequence s, int start, int end)

Appends a slice of a CharSequence. I use this to avoid temporary substrings.

public class AppendCharSequenceSliceDemo {

public static void main(String[] args) {

CharSequence header = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";

// Append only the scheme: "Bearer"

StringBuffer sb = new StringBuffer("scheme=");

sb.append(header, 0, 6);

System.out.println(sb);

}

}

Two practical tips I’ve learned the hard way:

  • start is inclusive and end is exclusive (same convention as substring).
  • I validate indices when they come from user input or parsed data. An IndexOutOfBoundsException in a hot path can turn into a noisy 500.

13) append(Object obj)

This calls String.valueOf(obj), which means:

  • null becomes "null".
  • Otherwise, it calls obj.toString().

The real-world rule I follow: if toString() output is part of a stable format (logs, cache keys, file names), be deliberate. Default toString() on some types can be noisy or unstable.

import java.util.UUID;

public class AppendObjectDemo {

public static void main(String[] args) {

UUID id = UUID.fromString("123e4567-e89b-12d3-a456-426614174000");

StringBuffer sb = new StringBuffer("id=");

sb.append(id); // UUID.toString()

System.out.println(sb);

Object missing = null;

sb = new StringBuffer("missing=");

sb.append(missing);

System.out.println(sb); // missing=null

}

}

Bonus overload many people forget: appendCodePoint(int codePoint)

This one matters if you deal with full Unicode characters beyond the BMP (Basic Multilingual Plane), like many emoji. A char is 16-bit; some characters require two chars (a surrogate pair). appendCodePoint handles that correctly.

public class AppendCodePointDemo {

public static void main(String[] args) {

int rocket = 0x1F680; // 🚀

StringBuffer sb = new StringBuffer("status=");

sb.append("ready ");

sb.appendCodePoint(rocket);

System.out.println(sb);

// Contrast with appending a single char: this is not a valid emoji

sb = new StringBuffer("broken=");

sb.append((char) rocket);

System.out.println(sb);

}

}

#### Code points vs “what users see” (grapheme clusters)

Even appendCodePoint has a boundary: a “character” on screen can be multiple code points.

For example:

  • Many emoji are a sequence of code points joined by a zero-width joiner.
  • Accented letters can be a base letter + combining mark.

That doesn’t make appendCodePoint useless—it’s still the right tool when you do have a code point and want valid UTF-16. I just don’t confuse “code point” with “user-perceived character.” When correctness is critical (for example, truncating user-visible strings without cutting emojis in half), I reach for higher-level libraries that understand grapheme clusters.

Common mistakes I see in reviews (and what I do instead)

Mistake 1: assuming null becomes empty

As you saw, many overloads append "null".

  • If "null" is acceptable (debug logs), I leave it.
  • If it’s user-visible text or a stable identifier, I guard it.

Patterns I use for stable output:

  • sb.append(value == null ? "" : value);
  • sb.append(Objects.toString(value, ""));

Mistake 2: building CSV or JSON without separator discipline

People append fields and forget commas/spaces, then patch it with if-statements all over.

For simple lists, I like the “separator after the first element” approach:

public class SeparatorDisciplineDemo {

public static void main(String[] args) {

String[] tags = {"priority=high", "owner=payments", "region=us-east"};

StringBuffer sb = new StringBuffer();

for (int i = 0; i < tags.length; i++) {

if (i > 0) sb.append(", ");

sb.append(tags[i]);

}

System.out.println(sb);

}

}

When the structure gets more complex (nested objects, quoting rules), I stop hand-building and use a real encoder. append() is a great low-level tool, but it’s not a replacement for a JSON library.

Mistake 3: using append(char[]) when you meant a slice

If you reuse a char buffer (common in parsers), append(char[]) can accidentally append stale tail data. I use the slice overload whenever the valid length is tracked (append(buf, 0, n)).

Mistake 4: sharing a single StringBuffer across unrelated work

Yes, StringBuffer is synchronized, but correctness still matters:

  • If two threads append unrelated data into one buffer, your output interleaves.
  • Synchronized does not mean “your higher-level format is safe.”

When multiple threads produce messages, I prefer:

  • each thread builds its own local buffer, then hands off the final string, or
  • a queue of immutable strings.

I’ll show a concrete example of that interleaving problem in a dedicated section below.

Mistake 5: treating append(double) as formatting

If you need currency, percent, or fixed decimals, format explicitly.

import java.text.DecimalFormat;

public class ExplicitFormattingDemo {

public static void main(String[] args) {

double price = 19.9;

DecimalFormat df = new DecimalFormat("0.00");

StringBuffer sb = new StringBuffer("price=");

sb.append(df.format(price));

System.out.println(sb); // price=19.90

}

}

Mistake 6: forgetting that append() mutates the same instance

Because append() returns this, chaining is ergonomic—but it’s easy to accidentally reuse a buffer you meant to discard.

The bug pattern looks like this:

  • you build a string in a helper,
  • you keep the StringBuffer around,
  • you append again later without resetting it.

When I reuse a buffer, I’m explicit:

  • sb.setLength(0) to clear contents,
  • or I create a new buffer per message for readability.

When I choose StringBuffer vs StringBuilder in 2026

Here’s the rule I actually follow:

  • If you’re building a string within one thread, pick StringBuilder.
  • If you truly need shared mutable append operations across threads on the same instance, pick StringBuffer.

A lot of modern Java code builds strings in request-scoped, method-local contexts. That makes StringBuilder the default. In the rare case you’re appending from multiple threads into the same object, I still ask if the design is right—often it’s cleaner to have threads produce their own strings and combine them deterministically.

Traditional vs modern choice (what I recommend)

Goal

Traditional choice

What I recommend most often —

— Build a string in a loop (single thread)

StringBuffer

StringBuilder Shared mutable buffer across threads

StringBuffer

StringBuffer, or redesign to avoid shared mutation Join many parts with separators

manual append()

StringJoiner / StringBuilder, or a real encoder for CSV/JSON Produce human-formatted numbers/dates

direct append(double)

NumberFormat / DateTimeFormatter, then append(String) Build large text from I/O

StringBuffer

StringBuilder (single thread) + append(char[],0,n)

Capacity planning: ensureCapacity, resizing, and clearing buffers safely

If you build large strings repeatedly (for example, serializing records in a batch job), two practices keep things fast and readable:

1) Pre-size when you can.

2) Reuse buffers carefully.

Pre-sizing with constructors and ensureCapacity()

I use the constructor when I have a decent estimate:

StringBuffer sb = new StringBuffer(256);

If I’m about to append something large and only know the minimum size at runtime, I use ensureCapacity:

public class EnsureCapacityDemo {

public static void main(String[] args) {

String payload = "x".repeat(1000);

StringBuffer sb = new StringBuffer();

sb.ensureCapacity(payload.length() + 32);

sb.append("payload=").append(payload);

System.out.println("length=" + sb.length() + " capacity=" + sb.capacity());

}

}

I don’t obsess over exact numbers. The goal is to reduce the number of times the buffer must grow in the critical loop.

Clearing a buffer: setLength(0) vs allocating a new one

If I’m in a performance-sensitive loop and I want to reuse a buffer, I clear it like this:

sb.setLength(0);

That resets the length to zero but usually keeps the allocated capacity around for reuse.

I allocate a new buffer when:

  • the code is not hot, and readability matters more, or
  • I’m worried about accidentally leaking data across iterations.

Thread safety: what StringBuffer guarantees (and what it doesn’t)

People often hear “StringBuffer is thread-safe” and assume it’s safe to use a single instance as a shared log builder. It’s not that simple.

What synchronized methods do guarantee

Because methods are synchronized, each call like append("x") is atomic with respect to other threads calling append(...) on the same instance.

That means you won’t corrupt the internal character array.

What it does not guarantee: atomicity across multiple appends

If your “message” requires multiple appends, other threads can slip in between them.

Here’s a demo that makes the bug obvious:

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.TimeUnit;

public class InterleavingDemo {

public static void main(String[] args) throws InterruptedException {

StringBuffer shared = new StringBuffer();

ExecutorService pool = Executors.newFixedThreadPool(2);

Runnable taskA = () -> {

for (int i = 0; i < 5; i++) {

shared.append("A[").append(i).append("] ");

}

};

Runnable taskB = () -> {

for (int i = 0; i < 5; i++) {

shared.append("B[").append(i).append("] ");

}

};

pool.submit(taskA);

pool.submit(taskB);

pool.shutdown();

pool.awaitTermination(2, TimeUnit.SECONDS);

System.out.println(shared);

}

}

The output will vary run to run, and it may interleave pieces of A and B. The buffer stays structurally valid, but your higher-level “format” is not protected.

How I fix it in real code

I choose one of these approaches:

1) Build per-thread and merge:

  • each thread uses a local StringBuilder (or StringBuffer if needed),
  • then you append the final string to a thread-safe queue or logger.

2) If you must share one buffer, synchronize at a higher level:

public class HigherLevelSyncDemo {

private final StringBuffer shared = new StringBuffer();

private final Object lock = new Object();

public void appendMessage(String who, int i) {

synchronized (lock) {

shared.append(who).append("[").append(i).append("] ");

}

}

}

The key is that your message is the atomic unit, not each individual append call.

Formatting: when I don’t append primitives directly

append(int) and append(double) are great for diagnostics, but formatting is a separate problem.

Currency/percent/locale output

If the string is meant for humans, I format with NumberFormat:

import java.text.NumberFormat;

import java.util.Locale;

public class NumberFormatDemo {

public static void main(String[] args) {

double price = 1234.5;

NumberFormat usd = NumberFormat.getCurrencyInstance(Locale.US);

StringBuffer sb = new StringBuffer("total=");

sb.append(usd.format(price));

System.out.println(sb);

}

}

Dates and times

For modern Java time, I format with java.time.format.DateTimeFormatter:

import java.time.Instant;

import java.time.ZoneId;

import java.time.format.DateTimeFormatter;

public class DateTimeFormatterDemo {

public static void main(String[] args) {

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

.withZone(ZoneId.of("UTC"));

StringBuffer sb = new StringBuffer("ts=");

sb.append(fmt.format(Instant.now()));

System.out.println(sb);

}

}

I treat append() as a compositor. If a value has formatting rules, I format it once, then append the resulting string.

Building structured text safely: CSV and JSON-style escaping

I’m careful about using raw append() when the output format has strict escaping rules. Two examples show why.

Example 1: CSV needs quoting rules

If a field can contain commas, quotes, or newlines, you need to quote and escape it. Here’s a small helper that covers the common case:

public class CsvAppendDemo {

static void appendCsvField(StringBuffer sb, String field) {

if (field == null) {

sb.append("");

return;

}

boolean needsQuotes = field.indexOf(‘,‘) >= 0 |

field.indexOf(‘"‘) >= 0 field.indexOf(‘\n‘) >= 0

field.indexOf(‘\r‘) >= 0;

if (!needsQuotes) {

sb.append(field);

return;

}

sb.append(‘"‘);

for (int i = 0; i < field.length(); i++) {

char c = field.charAt(i);

if (c == ‘"‘) sb.append(‘"‘); // escape quote by doubling

sb.append(c);

}

sb.append(‘"‘);

}

public static void main(String[] args) {

StringBuffer sb = new StringBuffer();

appendCsvField(sb, "simple"); sb.append(‘,‘);

appendCsvField(sb, "a,b"); sb.append(‘,‘);

appendCsvField(sb, "he said \"hi\"");

System.out.println(sb);

}

}

Notice how append(char) and append(String) combine naturally. The important part is the discipline: define your quoting rules once and reuse them.

Example 2: JSON-like escaping (good for logs)

If you’re building JSON manually, I still recommend a proper library. But for “JSON-ish” log output, a minimal escape helper is sometimes enough:

public class JsonEscapeDemo {

static void appendJsonString(StringBuffer sb, String s) {

if (s == null) {

sb.append("null");

return;

}

sb.append(‘"‘);

for (int i = 0; i < s.length(); i++) {

char c = s.charAt(i);

switch (c) {

case ‘"‘: sb.append("\\\""); break;

case ‘\\‘: sb.append("\\\\"); break;

case ‘\n‘: sb.append("\\n"); break;

case ‘\r‘: sb.append("\\r"); break;

case ‘\t‘: sb.append("\\t"); break;

default:

if (c < 0x20) {

String hex = Integer.toHexString(c);

sb.append("\\u");

for (int k = hex.length(); k < 4; k++) sb.append('0');

sb.append(hex);

} else {

sb.append(c);

}

}

}

sb.append(‘"‘);

}

public static void main(String[] args) {

StringBuffer sb = new StringBuffer();

sb.append("{\"msg\":");

appendJsonString(sb, "line1\nline2");

sb.append(",\"ok\":").append(true).append(‘}‘);

System.out.println(sb);

}

}

This example also shows a practical advantage of StringBuffer: you can build output without allocating a bunch of intermediate strings.

Unicode and append(): troubleshooting weird characters in output

When output looks “slightly wrong,” I usually check three things:

1) Did someone append a lone surrogate (append((char) codePoint) instead of appendCodePoint)?

2) Is the string normalized (for example, composed vs decomposed accents)?

3) Did the bytes get decoded with the wrong charset before they ever reached the buffer?

append() itself is rarely the source of encoding bugs—it just appends UTF-16 characters. But it can reveal bugs that were already present.

A quick surrogate sanity check

If you ever have an int that represents a code point, I do this:

  • validate it with Character.isValidCodePoint(codePoint),
  • append it with appendCodePoint(codePoint).

public class CodePointValidationDemo {

public static void main(String[] args) {

int cp = 0x1F680;

StringBuffer sb = new StringBuffer();

if (Character.isValidCodePoint(cp)) {

sb.appendCodePoint(cp);

} else {

sb.append("?");

}

System.out.println(sb);

}

}

Performance notes: what I optimize (and what I don’t)

I keep performance advice practical:

  • I don’t micro-optimize a string builder in code that runs once.
  • I do optimize string building in tight loops, logging hot paths, and serialization.

My rule of thumb

If you append inside a loop, I do three things:

1) Use a mutable builder (StringBuilder or StringBuffer).

2) Pre-size when reasonable.

3) Avoid unnecessary temporary strings (use the right overload).

Benchmarking the right way (briefly)

If you want real numbers, I use JMH (Java Microbenchmark Harness). I avoid “benchmarking” with System.nanoTime() inside main() because JIT warmup and dead-code elimination can make results misleading.

I’ll keep it conceptual here: StringBuffer is often slower than StringBuilder for single-threaded work because of synchronization, but both are typically far better than repeated String concatenation in a loop.

Quick reference: choosing the right append overload

Here’s the checklist I follow in code review:

  • You have a primitive? Use the primitive overload (append(int), append(long), etc.).
  • You have a char[] buffer plus a valid length? Use append(buf, 0, n).
  • You have a CharSequence and you only need part of it? Use append(seq, start, end).
  • You have an object and you control its formatting? Convert explicitly, then append(String).
  • You have a Unicode code point? Use appendCodePoint(codePoint).

A few paste-ready mini examples

Sometimes you just want quick patterns you can drop into code.

1) Build a cache key predictably

public class CacheKeyDemo {

public static void main(String[] args) {

String tenant = "t1";

String user = "u9";

int shard = 3;

StringBuffer key = new StringBuffer(64);

key.append("tenant:").append(tenant)

.append("|user:").append(user)

.append("|shard:").append(shard);

System.out.println(key);

}

}

2) Avoid temporary substring() allocations

public class AvoidSubstringDemo {

public static void main(String[] args) {

String token = "Bearer abc.def.ghi";

StringBuffer sb = new StringBuffer();

int space = token.indexOf(‘ ‘);

if (space > 0) {

sb.append(token, 0, space); // uses CharSequence slice overload

}

System.out.println(sb);

}

}

3) Reuse one buffer safely in a loop

public class ReuseBufferDemo {

public static void main(String[] args) {

StringBuffer sb = new StringBuffer(64);

for (int i = 0; i < 3; i++) {

sb.setLength(0);

sb.append("record=").append(i);

System.out.println(sb.toString());

}

}

}

Final takeaways

  • StringBuffer.append() is a flexible, overload-heavy API that lets you append primitives, text, arrays, and objects without manual conversion.
  • The two edge cases I always watch for are null (becomes "null") and Unicode (use appendCodePoint when you have code points).
  • StringBuffer is synchronized. That makes it safe for concurrent mutation, but it doesn’t magically prevent higher-level message interleaving.
  • For single-threaded string building, I default to StringBuilder. I reach for StringBuffer when shared, synchronized mutation is genuinely needed.

If you tell me what you’re building (logs, CSV export, protocol frames, HTML, etc.), I can show a more specialized append() pattern and a short checklist of pitfalls for that format.

Scroll to Top