Fundamentals 9 min read

Understanding and Implementing Java's StringJoiner: Source Code Analysis and Custom Usage

This article examines why a dedicated StringJoiner helper class is useful, compares it with StringBuilder, explores rarely used features, and provides a detailed analysis of the JDK implementation—including member variables, constructors, add, toString, merge, length, and custom empty value handling—along with practical code examples.

Architect's Tech Stack
Architect's Tech Stack
Architect's Tech Stack
Understanding and Implementing Java's StringJoiner: Source Code Analysis and Custom Usage

The author introduces the motivation for a dedicated string‑joining helper class, noting that the traditional StringBuilder is rigid and lacks built‑in splitting, requiring manual comma handling.

StringBuilder sb = new StringBuilder();
IntStream.range(1,10).forEach(i -> {
    sb.append(i+"");
    if(i < 10){
        sb.append(",");
    }
});

Using StringJoiner simplifies this pattern:

StringJoiner sj = new StringJoiner(",");
IntStream.range(1,10).forEach(i -> sj.add(i+""));

The article lists less‑known functionalities of StringJoiner such as setEmptyValue (custom empty value), merge (combine another joiner), and length (current length including prefix/suffix).

Two implementation ideas are discussed: maintaining a List and joining at toString() (simple but memory‑heavy), or enhancing StringBuilder as the JDK does.

JDK source analysis – member variables :

private final String prefix;
private final String delimiter;
private final String suffix;

/*
 * StringBuilder value – at any time, the characters constructed from the
 * prefix, the added element separated by the delimiter, but without the
 * suffix, so that we can more easily add elements without having to jigger
 * the suffix each time.
 */
private StringBuilder value;

/*
 * By default, the string consisting of prefix+suffix, returned by
 * toString(), or properties of value, when no elements have yet been added,
 * i.e. when it is empty. This may be overridden by the user to be some
 * other value including the empty String.
 */
private String emptyValue;

Constructor :

public StringJoiner(CharSequence delimiter,
                     CharSequence prefix,
                     CharSequence suffix) {
    Objects.requireNonNull(prefix, "The prefix must not be null");
    Objects.requireNonNull(delimiter, "The delimiter must not be null");
    Objects.requireNonNull(suffix, "The suffix must not be null");
    // make defensive copies of arguments
    this.prefix = prefix.toString();
    this.delimiter = delimiter.toString();
    this.suffix = suffix.toString();
    // !!!构造时就直接将emptyValue拼接好了。
    this.emptyValue = this.prefix + this.suffix;
}

Adding elements :

public StringJoiner add(CharSequence newElement) {
    prepareBuilder().append(newElement);
    return this;
}

private StringBuilder prepareBuilder() {
    if (value != null) {
        // already have elements, prepend delimiter
        value.append(delimiter);
    } else {
        // first element, prepend prefix
        value = new StringBuilder().append(prefix);
    }
    return value;
}

toString implementation :

public String toString() {
    if (value == null) {
        // no elements added – return emptyValue (prefix+suffix by default)
        return emptyValue;
    } else {
        if (suffix.equals("")) {
            return value.toString();
        } else {
            int initialLength = value.length();
            String result = value.append(suffix).toString();
            // reset value to pre‑append length
            value.setLength(initialLength);
            return result;
        }
    }
}

The design avoids appending the suffix during element addition, making toString() and length() cheap and enabling efficient merge operations.

merge method :

public StringJoiner merge(StringJoiner other) {
    Objects.requireNonNull(other);
    if (other.value != null) {
        final int length = other.value.length();
        // lock the length to avoid interference when merging with itself
        StringBuilder builder = prepareBuilder();
        builder.append(other.value, other.prefix.length(), length);
    }
    return this;
}

Because merge appends the other joiner’s internal StringBuilder without its prefix, the suffix cannot be appended directly in toString() ; otherwise the length calculations would be incorrect.

length method :

public int length() {
    // we never actually append the suffix unless we need the full string
    return (value != null ? value.length() + suffix.length() : emptyValue.length());
}

Finally, the article shows how to customize the empty value:

public StringJoiner setEmptyValue(CharSequence emptyValue) {
    // Objects.requireNonNull returns the argument itself after null‑check
    this.emptyValue = Objects.requireNonNull(emptyValue, "The empty value must not be null").toString();
    return this;
}

In summary, StringJoiner builds on StringBuilder by handling prefix, delimiter, and suffix lazily, which simplifies merging and length calculations while keeping memory usage reasonable.

backendJavacode-analysisStringBuilderStringJoiner
Architect's Tech Stack
Written by

Architect's Tech Stack

Java backend, microservices, distributed systems, containerized programming, and more.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.