Understanding Java's StringJoiner: Design, Implementation, and Practical Usage
This article explains why the StringJoiner class was introduced as a more flexible alternative to StringBuilder, details its internal fields, constructors, and core methods such as add, merge, toString, length, and setEmptyValue, and demonstrates their behavior with concrete Java code examples.
When reading project code, the author discovered the StringJoiner class and found it an interesting utility built on top of StringBuilder for easier string concatenation with delimiters.
Why a new helper class? The original StringBuilder is rigid and does not support automatic delimiters. To produce a comma‑separated string, developers had to manually insert commas, as shown in the following example:
StringBuilder sb = new StringBuilder();
IntStream.range(1,10).forEach(i->{
sb.append(i+"");
if(i < 10){
sb.append(",");
}
});Using StringJoiner the same task becomes much simpler:
StringJoiner sj = new StringJoiner(",");
IntStream.range(1,10).forEach(i -> sj.add(i+""));The class also provides less‑used features such as setEmptyValue , merge , and length , which are listed in the original article.
JDK Source Analysis – Member Variables
private final String prefix;
private final String delimiter;
private final String suffix;
private StringBuilder value;
private String emptyValue; // built from prefix+suffix when no elements are addedThese fields store the prefix, delimiter, suffix, the mutable StringBuilder that holds the current content, and the pre‑computed empty value.
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");
this.prefix = prefix.toString();
this.delimiter = delimiter.toString();
this.suffix = suffix.toString();
// !!!构造时就直接将emptyValue拼接好了。
this.emptyValue = this.prefix + this.suffix;
}The constructor eagerly builds emptyValue because most use‑cases never customize it.
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;
}The method ensures the prefix and delimiter are added automatically; the suffix is deliberately omitted until toString() is called.
toString Implementation
public String toString() {
if (value == null) {
// no elements added – return emptyValue (prefix+suffix)
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 suffix is appended only for the final string representation, preserving the builder for further modifications.
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;
}By copying only the part after the other joiner’s prefix, the method avoids duplicating delimiters and respects the internal state, which explains why toString() cannot simply concatenate value and suffix after a merge.
length Method
public int length() {
// suffix is not stored until toString() is called
return (value != null ? value.length() + suffix.length() : emptyValue.length());
}The length includes the prefix, delimiter, added elements, and suffix when present.
setEmptyValue
public StringJoiner setEmptyValue(CharSequence emptyValue) {
// Objects.requireNonNull returns the first argument after null‑check
this.emptyValue = Objects.requireNonNull(emptyValue, "The empty value must not be null").toString();
return this;
}This allows callers to customize the string returned when no elements have been added.
Overall, StringJoiner leverages StringBuilder internally, adds prefix and delimiter handling during add , defers suffix addition to toString() , and provides convenient merging and length calculations, making it a powerful utility for building delimited strings.
Architect's Tech Stack
Java backend, microservices, distributed systems, containerized programming, and more.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.