9.2 String Operations and Construction
Prerequisites: String basics, arrays, and loops
Estimated time: 30 minutes
Runtime Environment: Java 17
You are ready to continue when: You can extract, split, clean, replace, and build text according to its API contract
An order line looks like just a piece of text
An order line contains a date, customer, item, and quantity. Splitting and rebuilding it correctly depends on the API contract: whether an endpoint is included, whether a delimiter is a regular expression, and whether trailing empty fields are preserved. Snippets using Arrays, Pattern, Matcher, and related types need imports; the complete example lists them. Run snippets with repeated variable names separately.
1. substring Includes the Start and Excludes the End
String order = "2026-07-15|a developer|Hammer|2";
String date = order.substring(0, 10);
String details = order.substring(11);
System.out.println(date);
System.out.println(details);Output:
2026-07-15
a developer|Hammer|2substring(begin, end) includes begin but not end, and the effective scope satisfies:
0 <= begin <= end <= text.length()Out-of-range indexes or begin > end cause an IndexOutOfBoundsException under the API contract; common JDK implementations use its subclass StringIndexOutOfBoundsException. Indexes are counted in UTF-16 code units; if the split point falls in the middle of a surrogate pair, an incomplete Unicode character is obtained. When locating by code point, first use offsetByCodePoints to compute the index.
The substring contract specifies which text is returned, not how backing storage is allocated or shared. A full-range substring may even return the original object. For memory analysis, inspect the target JDK and retained objects rather than inferring allocation size from substring length alone.
2. The first parameter of split is a regular expression
By default, a regex dot matches any character except a line terminator, so you can't simply write:
String ip = "192.168.1.1";
System.out.println(Arrays.toString(ip.split(".")));
System.out.println(Arrays.toString(ip.split("\\.")));Output:
[]
[192, 168, 1, 1]When delimiters come from configuration or user input, don't guess which characters need escaping on your own:
String delimiter = ".";
String[] parts = ip.split(Pattern.quote(delimiter));The second parameter determines the number of splits and trailing empty items.
String row = "Hammer,2,,";
System.out.println(Arrays.toString(row.split(",")));
System.out.println(Arrays.toString(row.split(",", -1)));
System.out.println(Arrays.toString(row.split(",", 3)));Output:
[Hammer, 2]
[Hammer, 2, , ]
[Hammer, 2, ,]- Don't pass limit or pass
0: discard trailing empty strings; - Negative: retain all fields, including trailing empty fields;
- Positive
n: cut into at mostnpieces, with the last piece retaining the uncut content.
CSV has quoting, escaping, and embedded newline rules that cannot be correctly parsed with just one split(","). Production code should use a library that conforms to the target CSV dialect.
3. trim vs. strip Have Different Blank Definitions
String padded = " \u2003Hello\u2003 ";
System.out.println("'" + padded.trim() + "'");
System.out.println("'" + padded.strip() + "'");Output:
' Hello '
'Hello'trim() removes leading and trailing characters with code values no greater than U+0020. Added in Java 11, strip() uses Character.isWhitespace, which handles more multilingual whitespace than trim. Some space-like characters still do not match that definition: "\u00A0".strip().isEmpty() is false because the non-breaking space U+00A0 is excluded. When choosing a method, consider the input protocol rather than broadly stating that strip clears all Unicode whitespace.
Related methods also include:
text.stripLeading();
text.stripTrailing();
text.isBlank();isBlank() accepts empty strings or strings composed entirely of Character.isWhitespace characters; isEmpty() only checks if the length is 0.
4. replace processes literal text, replaceAll processes regex
String notice = "Cats and cats";
System.out.println(notice.replace("cats", "dogs"));
System.out.println(notice.replaceAll("(?i)cats", "dogs"));Output:
Cats and dogs
dogs and dogsreplace(CharSequence, CharSequence) treats the target as literal text. The first parameter of replaceAll(regex, replacement) is a regex, and the second parameter has its own replacement syntax: $1 references capture groups, and backslashes are used for escaping.
When replacing text from external input, use Matcher.quoteReplacement:
String price = "price=10";
String safe = price.replaceAll("\\d+", Matcher.quoteReplacement("$5"));
System.out.println(safe); // price=$5If you only need to replace literal content, prefer replace, it's straightforward and bypasses two layers of regex rules.
5. Search and Case Conversion
| Method | Result | Boundary |
|---|---|---|
indexOf(text) | First appearance location, not found at -1 | Starting index can be specified |
lastIndexOf(text) | Last appearance position | Not found for -1 |
contains(text) | Whether the string contains a literal substring | Parameter cannot be null |
startsWith / endsWith | Matches prefix or suffix | Case sensitive |
toUpperCase(locale) | Convert based on specified region | When targeting users, do not rely on the default region |
When a protocol permits this conversion, Locale.ROOT makes its result independent of the machine's default locale. The protocol still defines which characters are allowed and whether case matters:
String normalized = command.toUpperCase(Locale.ROOT);Natural language search, sorting, and case folding are more complex and cannot assume that a single toLowerCase() resolves all Unicode equivalence issues.
6. Choosing the Right Construction Method
Small Fixed Fragments: Using +
String label = "Order #" + orderId + ": " + itemName;The code is clear; the compiler handles runtime concatenation strategy. Don't manually write a Builder just to combine two fragments.
Step-by-step appending in loops or branches: using StringBuilder
StringBuilder report = new StringBuilder();
for (OrderLine line : lines) {
report.append(line.item())
.append(" x ")
.append(line.quantity())
.append(System.lineSeparator());
}
String text = report.toString();Capacity grows automatically. Only provide an initial capacity when you have a reliable estimate and measurement shows that avoiding growth would help; arbitrarily setting a large capacity may waste memory.
Existing collection: use join
String summary = String.join(" | ", "Hammer", "Nail", "Plank");
String commaSeparated = names.stream().collect(Collectors.joining(","));This only joins names with commas; it does not escape commas, quotes, or newlines inside fields, so it is not a complete CSV encoder. When you need both a prefix and a suffix, Collectors.joining(", ", "[", "]") is more reliable than manually handling the last delimiter.
Template text: Format or text block
String receipt = "Order %s, Total %d item".formatted(orderId, quantity);
String template = """
Customer:%s
Item:%s
""".formatted(customer, item);Format strings also have type contracts; external input should not be used directly as a trusted template.
7. StringBuilder and StringBuffer
| Type | Concurrency Contract | Common Uses |
|---|---|---|
StringBuilder | No thread-safety guarantee | mutable buffer owned by method or single-threaded context |
StringBuffer | Synchronizes where needed for individual thread-safe operations | Must comply with compatibility requirements for older APIs of this type |
StringBuffer synchronizes where needed, but a sequence such as "check then append" is not one atomic operation. Appending another mutable CharSequence also does not automatically protect that source object just because the target buffer is synchronized. Sharing a mutable text buffer across multiple threads typically entangles ordering, locking, and error handling. A more common design is for each task to locally construct its result, and then a clear consolidation phase to merge them.
Don't state fixed conclusions like "2 to 3 times slower" or "99% of the time." Performance differences depend on the JDK, contention, and workload; use tools like JMH to measure target code when performance requirements exist.
8. Complete Working Example
First, prepare a temporary directory:
chapter9_operations_root=$(mktemp -d)
readonly chapter9_operations_root
mkdir -p "$chapter9_operations_root/out"
cd "$chapter9_operations_root"Save as StringOperationsDemo.java:
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class StringOperationsDemo {
public static void main(String[] args) {
String order = "2026-07-15|a developer|Hammer|2";
checkEquals("2026-07-15", order.substring(0, 10));
checkEquals("a developer|Hammer|2", order.substring(11));
String ip = "192.168.1.1";
checkEquals(List.of("192", "168", "1", "1"),
Arrays.asList(ip.split(Pattern.quote("."))));
String row = "Hammer,2,,";
checkEquals(2, row.split(",").length);
checkEquals(4, row.split(",", -1).length);
checkEquals("", row.split(",", -1)[3]);
String padded = " \u2003Hello\u2003 ";
checkEquals("\u2003Hello\u2003", padded.trim());
checkEquals("Hello", padded.strip());
String safePrice = "price=10".replaceAll(
"\\d+", Matcher.quoteReplacement("$5"));
checkEquals("price=$5", safePrice);
List<String> items = List.of("Hammer", "Nail", "Plank");
String joined = items.stream().collect(Collectors.joining(" | "));
checkEquals("Hammer | Nail | Plank", joined);
StringBuilder report = new StringBuilder();
for (int index = 0; index < items.size(); index++) {
report.append(index + 1).append(". ").append(items.get(index));
if (index + 1 < items.size()) report.append(System.lineSeparator());
}
System.out.println(order.substring(0, 10));
System.out.println(joined);
System.out.println("REPORT".toLowerCase(Locale.ROOT) + ":");
System.out.println(report);
System.out.println("String operation check passed");
}
static void checkEquals(Object expected, Object actual) {
if (!Objects.equals(expected, actual)) {
throw new AssertionError("expected=" + expected + ", actual=" + actual);
}
}
}Run:
javac --release 17 -encoding UTF-8 -Xlint:all -d out StringOperationsDemo.java
java -cp out StringOperationsDemoExpected output:
2026-07-15
Hammer | Nail | Plank
report:
1. Hammer
2. Nail
3. Plank
String operation check passedClean up after saving the necessary experiment records:
cd
ls -ld -- "$chapter9_operations_root"
rm -r -- "$chapter9_operations_root"Professional Lookup Entry
StringAPI:substring,split,strip,formatted,lines;- The different uses of
Pattern.quoteandMatcher.quoteReplacement; - The capacity, length, and
setLengthagreement ofStringBuilder; - JMH: Use the benchmarking framework to compare concatenation strategies, avoid drawing conclusions based on a single
nanoTime.
Before leaving this chapter
Write the inclusive-start, exclusive-end bounds for substring. Test a regex dot, trailing empty fields, and a positive limit with split; pass external replacement values through Matcher.quoteReplacement. Then, based on scenario, select a few +, incremental StringBuilder, or collection joins, and explicitly state that StringBuffer's single-method synchronization does not equal full process atomicity.
Keep the fixed inputs and expected fields. When the order format changes, use them to identify which parsing rules need to change. The next chapter introduces collections for data whose size can grow.
API Entry: Java 17 Pattern, Java 17 Matcher, Java 17 StringBuilder.