Table of Contents
- Table of Contents
- Introduction
- Unit Test
- Construction
- Basic String Contents
- Appending the Names
- Adding Comma and Space Separators
- Removing Duplicates, Adding And
- Filtering and Sorting
- Summary
Introduction
When we cannot fully construct a String value in one go, then the String.format approach from the prior post will not fulfill our needs. However, if we are sure that only one Java thread will be manipulating the contents used to construct the desired String value, then we could use Java’s StringBuilder to efficiently build and store our interim character contents until the final String value is ready to be built. For example, a StringBuilder could be used within a single method as a local variable, since only
A mutable sequence of characters. This class provides an API compatible with
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/StringBuilder.htmlStringBuffer, but with no guarantee of synchronization. This class is designed for use as a drop-in replacement forStringBufferin places where the string buffer was being used by a single thread (as is generally the case). Where possible, it is recommended that this class be used in preference toStringBufferas it will be faster under most implementations.
Unit Test
Let’s add some unit tests to confirm when our new method has the desired functionality. Let’s add the following to the java101/src/test/java/codingchica/java101/AppTest.java:
@Nested
class GetGreetingForMultipleNamesTest {
@ParameterizedTest
@NullAndEmptySource
void getGreetingForMultipleNames_whenNamesNullOrEmpty_thenReturnsExpectedValue(String[] names) {
// Setup
String expectedResult = "Hello!";
// Execution
String actualGreeting = App.getGreetingForMultipleNames(names);
// Validation
assertEquals(expectedResult, actualGreeting);
}
@Test
void getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue() {
// Setup
String[] names =
new String[] {
/* Filtered out */ null,
"Jane",
/* filtered out */ "",
"Sally",
/* filtered out */ " ",
/* duplicate filtered out */ "Jane",
/* Sorted to the beginning.*/ "Bonnie",
};
String expectedResult = "Hello Bonnie, Jane, and Sally!";
// Execution
String actualGreeting = App.getGreetingForMultipleNames(names);
// Validation
assertEquals(expectedResult, actualGreeting);
}
}
Construction
For the sake of performance, it is best if we have a rough idea of how large a String we may be building and initialize the StringBuilder to the corresponding capacity (or slightly larger) so that:
- The StringBuilder only consumes the required space, or just a little more, and
- The StringBuilder will not have to be resized as the desired contents are added to it while building the String.
Let’s start by constructing a new StringBuilder inside a new method within the java101/src/main/java/codingchica/java101/App.java. Since we are creating a local variable inside the method and each method invocation will receive its own copies of local variables, we are safe to use the StringBuilder:
/** The initial capacity expected for a greeting String. */
private static final int INITIAL_GREETING_CAPACITY = 50;
...
/**
* A utility function to generate a greeting for multiple names.
*
* @param namesToGreet The list of names to include in the greeting
* @return A greeting in english with names, if available.
*/
public static String getGreetingForMultipleNames(String[] namesToGreet) {
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
return null;
}
Build Failure
If we run the Maven build (mvn clean site install) now to test the functionality, we should expect a failure like:
[ERROR] Failures: [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesNullOrEmpty_thenReturnsExpectedValue:307 expected: <Hello!> but was: <null> [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesNullOrEmpty_thenReturnsExpectedValue:307 expected: <Hello!> but was: <null> [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue:329 expected: <Hello Bonnie, Jane, and Sally!> but was: <null>
Basic String Contents
Let’s add enough runtime code to the new method to generate the basic greeting – without any names included:
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
String prefix = "Hello";
stringBuilder.append(prefix);
stringBuilder.append('!');
return stringBuilder.toString();
Build Failure
If we rerun the Maven build (mvn clean site install) again, then we should have eliminated all but one failure and the error message for that should have changed:
[ERROR] Failures: [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue:329 expected: <Hello Bonnie, Jane, and Sally!> but was: <Hello!>
Appending the Names
Now, let’s loop through the names to add them to the String:
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
String prefix = "Hello";
stringBuilder.append(prefix);
if (namesToGreet != null) {
Arrays.stream(namesToGreet).forEach(stringBuilder::append);
}
stringBuilder.append('!');
return stringBuilder.toString();
Build Failure
At this point, rerunning the Maven build (mvn clean site install) will result in an error message like the following:
[ERROR] Failures: [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue:329 expected: <Hello Bonnie, Jane, and Sally!> but was: <HellonullJaneSally JaneBonnie!>
The output is improving, but we are still a ways away from the expected result.
Adding Comma and Space Separators
Now, let’s separate the names with commas and spaces:
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
String prefix = "Hello";
stringBuilder.append(prefix);
if (namesToGreet != null) {
Arrays.stream(namesToGreet).forEach(name -> {
// Separate names with a comma
if (stringBuilder.length() > prefix.length()) {
stringBuilder.append(',');
}
// Space between each segment
stringBuilder.append(' ');
stringBuilder.append(name);
});
}
stringBuilder.append('!');
return stringBuilder.toString();
Build Failure
Now, running the Maven build (mvn clean site install) results in a different error:
[ERROR] Failures: [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue:329 expected: <Hello Bonnie, Jane, and Sally!> but was: <Hello null, Jane, , Sally, , Jane, Bonnie!>
Removing Duplicates, Adding And
Now, let’s remove the second Jane from our test greeting by ignoring duplicate values, as well as adding the “and ” before the last name:
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
String prefix = "Hello";
stringBuilder.append(prefix);
if (namesToGreet != null) {
List<String> distinctNames = Arrays.stream(namesToGreet)
.distinct() // Filter out duplicate values
.toList(); // Save as a list
String lastValue =
distinctNames.stream()
.reduce((first, second) -> second) // Find last value
.orElseGet(() -> null); // Default to null
distinctNames
.forEach(
name -> {
// Separate names with a comma
if (stringBuilder.length() > prefix.length()) {
stringBuilder.append(',');
}
// Space between each segment
stringBuilder.append(' ');
// The last name should be preceded with and
if (lastValue != null && lastValue.equals(name)) {
stringBuilder.append("and ");
}
stringBuilder.append(name);
});
}
stringBuilder.append('!');
return stringBuilder.toString();
Build Failure
Running the Maven build (mvn clean site install) at this point will generate a different error message with the “and ” added only before the final name and the second “Jane” has been removed:
[ERROR] Failures: [ERROR] AppTest$GetGreetingForMultipleNamesTest.getGreetingForMultipleNames_whenNamesPopulated_thenReturnsExpectedValue:329 expected: <Hello Bonnie, Jane, and Sally!> but was: <Hello null, Jane, , Sally, , and Bonnie!>
Filtering and Sorting
Now, let’s add some filtering and sorting logic to get rid of some of the extra commas and the null entry, as well as alphabetizing the names in the output:
StringBuilder stringBuilder = new StringBuilder(INITIAL_GREETING_CAPACITY);
String prefix = "Hello";
stringBuilder.append(prefix);
if (namesToGreet != null) {
List<String> distinctNames =
Arrays.stream(namesToGreet)
.filter(Objects::nonNull) // Filter out null values
.filter(item -> !item.isBlank()) // Filter out blank strings
.distinct() // Filter out duplicate values
.sorted() // Alphabetized
.toList(); // Save as a list
String lastValue =
distinctNames.stream()
.reduce((first, second) -> second) // Find last value
.orElseGet(() -> null); // Default to null
distinctNames.forEach(
name -> {
// Separate names with a comma
if (stringBuilder.length() > prefix.length()) {
stringBuilder.append(',');
}
// Space between each segment
stringBuilder.append(' ');
// The last name should be preceded with and
if (lastValue != null && lastValue.equals(name)) {
stringBuilder.append("and ");
}
stringBuilder.append(name);
});
}
stringBuilder.append('!');
return stringBuilder.toString();
Build Success
This time, rerunning the Maven build (mvn clean site install) should result in success.
[INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 20.440 s
Summary
Without the StringBuilder, had we tried to use concatenation to generate a similar String, there would have been many wasted cycles for object creation and garbage collection, as we continued to modify the starting string:
- “Hello”
- “Hello “
- “Hello Bonnie”
- “Hello Bonnie,”
- “Hello Bonnie, “
- “Hello Bonnie, Jane”
- “Hello Bonnie, Jane,”
- “Hello Bonnie, Jane, “
- “Hello Bonnie, Jane, and “
- “Hello Bonnie, Jane, and Sally”
- “Hello Bonnie, Jane, and Sally!”
Instead, by using the StringBuilder, we store the interim state in a way that we need not create new objects that are immediately discarded. This saves our process work in both object creation and subsequent garbage collection. It is safe to use StringBuilder when we are sure that only one thread will be modifying the contents of the StringBuilder. The functional result is the same as if we used String concatenation (we return the same String as a result of calling the method). However, the performance for our function will be significantly improved by this approach.

Leave a comment