Chapter 9: Strings and Text Handling
Text is at the heart of most modern software, and Java provides rich, reliable tools for working with it. From simple messages printed to the console to complex document processing and data exchange, strings play a central role in every kind of Java application. Because text is such a common form of data, the Java platform offers a robust, efficient, and secure set of features for creating, manipulating, and comparing strings.
Unlike languages that treat strings as primitive data types, Java defines them as full-fledged objects of the String class. This design provides immutability (meaning once a string is created it cannot be changed), which guarantees consistency, simplifies reasoning about code, and improves performance through shared internal storage. When you modify a string in Java, you are really creating a new one, a fact that shapes many common programming patterns.
Alongside the String class, Java also provides mutable alternatives like StringBuilder and StringBuffer, which allow efficient construction or modification of text in memory. These classes make it easy to assemble dynamic messages, build file paths, or generate large data blocks without the overhead of creating new string objects each time.
This chapter explores how to create and use strings, how to perform concatenation and comparison, and how to work with text using methods from the standard library. You will also learn about common encoding considerations, such as Unicode and character sets, which ensure that Java programs can handle text in any human language with consistency and reliability.
By mastering Java’s string handling, you will be prepared to work confidently with input and output, file data, formatted text, and any feature that relies on precise control of characters and words. Whether you are processing user input, generating reports, or parsing structured data, understanding strings is essential to writing expressive, correct, and international-ready Java code.
+ inside loops can lead to inefficient code. For such cases, prefer StringBuilder, which avoids unnecessary object creation and improves performance significantly.
String literals and immutability
In Java, strings are represented by the String class, and the most common way to create one is with a string literal. A string literal is a sequence of characters enclosed in double quotes, for example "Java" or "Hello, world!". When the compiler encounters a string literal, it automatically creates a String object for it in a special area of memory called the string pool.
String greeting = "Hello, world!";
String language = "Java";
Strings created with the same literal value are automatically shared in the string pool. This means that two identical literals refer to the same object, conserving memory and improving performance. The following example demonstrates this behavior:
String a = "Java";
String b = "Java";
System.out.println(a == b); // true (same pooled object)
System.out.println(a.equals(b)); // true (same content)
Although both a and b contain the same value, the important distinction is that they point to the same object instance in memory. Java ensures that identical literals are interned and reused rather than recreated.
String.intern() method can be used to place a string explicitly into the string pool. This is rarely necessary in everyday code, but can be useful when managing large numbers of repeated string values.
One of the defining features of Java strings is immutability. Once a String object has been created, its contents can never be changed. Any operation that appears to modify a string actually produces a new String object instead.
String name = "Java";
name = name + " Language";
System.out.println(name); // "Java Language"
Here, concatenating " Language" does not alter the original "Java" string. Instead, a new String object containing "Java Language" is created, and the variable name now refers to it. The original string remains unchanged in memory until it is garbage collected.
This design makes strings thread-safe and predictable, since no code can accidentally alter their contents once created. It also allows the Java runtime to reuse string objects safely without fear of unintended modification.
+ operator in a loop can generate many intermediate objects. To build text dynamically, use StringBuilder or StringBuffer, which are designed for efficient mutable string manipulation.
StringBuilder and StringBuffer
When you need to construct or modify text repeatedly, creating new String objects each time can be inefficient. Because strings are immutable, every concatenation or insertion creates a new object and copies existing data, which wastes both time and memory in large or iterative operations. To solve this, Java provides two mutable alternatives: StringBuilder and StringBuffer.
Both classes let you build and modify strings efficiently by maintaining a resizable buffer of characters. You can append, insert, delete, and replace text directly within the same object, avoiding the overhead of constantly creating new strings. The main difference between the two lies in thread safety.
- StringBuilder is not synchronized and is therefore faster. It should be used in single-threaded code where no concurrent access occurs.
- StringBuffer is synchronized, meaning its methods are thread-safe. It is suitable for situations where multiple threads may access or modify the same buffer.
// Using StringBuilder
StringBuilder sb = new StringBuilder();
sb.append("Java");
sb.append(" ");
sb.append("Language");
String result = sb.toString();
System.out.println(result); // Java Language
The same task could be done with StringBuffer in exactly the same way:
// Using StringBuffer
StringBuffer sbuf = new StringBuffer();
sbuf.append("Hello");
sbuf.append(", ");
sbuf.append("world!");
System.out.println(sbuf.toString()); // Hello, world!
In most modern programs, StringBuilder is preferred because its performance is better in single-threaded environments, which covers the majority of cases. The StringBuffer class exists mainly for backward compatibility with older Java code that required synchronization.
StringBuilder and StringBuffer because their methods return the same object. This allows concise and readable construction of complex strings.
String query = new StringBuilder()
.append("SELECT * FROM users WHERE name = '")
.append("Alice")
.append("' AND active = 1;")
.toString();
System.out.println(query);
When you call toString(), the buffer’s current contents are converted into an immutable String. You can continue modifying the builder afterward without affecting the string already produced.
StringBuilder nor StringBuffer should be shared between threads unless properly synchronized. If multiple threads need to build text simultaneously, give each its own instance or use synchronized access to prevent data corruption.
Formatting text with printf and format
Java provides built-in tools for producing formatted text using the printf() and format() methods. These methods work similarly to C-style formatting functions, allowing you to insert variable values into strings using format specifiers. They are especially useful when generating structured output such as reports, tables, or logs.
The System.out.printf() method prints formatted text directly to standard output, while String.format() creates and returns a formatted string that you can store or reuse later.
int count = 5;
double price = 9.99;
String item = "Widget";
System.out.printf("You bought %d %s(s) for a total of $%.2f%n", count, item, count * price);
Here, each format specifier (beginning with %) represents a placeholder for a value:
| Specifier | Meaning | Example |
%d | Integer (decimal) | %d → 42 |
%f | Floating-point number | %.2f → 3.14 (2 decimal places) |
%s | String | %s → Hello |
%n | Platform-independent newline | Equivalent to \n but safer across systems |
%c | Single character | %c → A |
%b | Boolean value | %b → true |
Each specifier can include optional flags and width controls to align and format output neatly:
System.out.printf("|%10s|%5d|%8.2f|%n", "Item", 12, 4.5);
System.out.printf("|%-10s|%5d|%8.2f|%n", "Item", 12, 4.5);
The first example right-aligns text within a 10-character field, while the second left-aligns it using -. These formatting options make it easy to produce tabular text layouts directly from code.
String.format() when you need to create formatted text without printing it immediately. This is ideal for logs, messages, or dynamically generated strings.String message = String.format("User %s has %d new notifications.", "Alice", 3);
System.out.println(message);
Formatting specifiers also support advanced options for locale-aware output, letting you automatically adapt number and date formats to regional conventions. This is done by providing a Locale argument:
import java.util.Locale;
double value = 1234.56;
System.out.println(String.format(Locale.FRANCE, "Value: %,.2f", value));
System.out.println(String.format(Locale.US, "Value: %,.2f", value));
In this example, the comma and period are swapped depending on locale conventions (1 234,56 in France versus 1,234.56 in the US). The , flag adds grouping separators to large numbers automatically.
%d for a string) will cause a java.util.IllegalFormatConversionException at runtime.
Regular expressions
Regular expressions, often called regex, are a concise and powerful way to describe and manipulate patterns in text. They allow you to search, match, and replace sequences of characters according to rules you define. Java’s java.util.regex package provides a complete implementation of regular expressions through two main classes: Pattern and Matcher.
With regex, you can validate input (such as email addresses or postcodes), extract structured data from text, or perform advanced find-and-replace operations. Because Java’s regex engine is integrated directly into the standard library, you can use it anywhere without external dependencies.
\d+ (for digits) or [A-Z][a-z]+ (for capitalized words) can make powerful text-processing tools.
The pattern and matcher classes
The Pattern class represents a compiled regular expression, while the Matcher class applies that pattern to a given input string. To use them, you first compile a pattern, then create a matcher to search within text.
import java.util.regex.*;
String text = "The year is 2025.";
Pattern pattern = Pattern.compile("\\d+");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("Found number: " + matcher.group());
}
This code finds and prints the first sequence of digits in the text. The double backslashes are required because Java string literals also use backslashes for escape sequences, so "\\d+" represents the regex \d+ (one or more digits).
"\\s+" for whitespace instead of "\s+", which would cause a syntax error.
Common regex patterns
Regular expressions use a combination of literal characters and special symbols (called metacharacters) to define patterns. Here are some of the most useful constructs:
| Pattern | Meaning | Example |
. | Any single character (except newline) | a.b → matches “a_b” |
\d | Digit (0–9) | \d+ → “42” |
\w | Word character (letter, digit, or underscore) | \w+ → “hello_123” |
\s | Whitespace | \s+ → spaces or tabs |
[abc] | Any one of a, b, or c | b[aeiou]t → “bat”, “bet” |
[^abc] | Any character except a, b, or c | [^0-9] → any non-digit |
* | Zero or more occurrences | go* → “g”, “go”, “goo”, etc. |
+ | One or more occurrences | go+ → “go”, “goo”, etc. |
? | Zero or one occurrence (optional) | colou?r → “color” or “colour” |
{n,m} | Between n and m occurrences | \d{2,4} → 2 to 4 digits |
^ | Start of line | ^Hello → matches at beginning |
$ | End of line | world!$ → matches at end |
Combining these constructs lets you describe complex text structures concisely. For example:
// Match a simple email address
Pattern email = Pattern.compile("[\\w.-]+@[\\w.-]+\\.\\w+");
Matcher m = email.matcher("contact@example.com");
if (m.matches()) {
System.out.println("Valid email");
}
Searching and replacing
The Matcher class provides methods such as find(), matches(), group(), and replaceAll() for practical text operations. For instance, you can extract or transform multiple matches within a string:
String text = "cat, dog, bird";
Pattern pattern = Pattern.compile("\\w+");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println("Animal: " + matcher.group());
}
You can also perform replacements directly using regular expressions:
String input = "2025-10-26";
String result = input.replaceAll("-", "/");
System.out.println(result); // 2025/10/26
More complex replacements can use groups, where parts of the matched text are captured and reinserted using $1, $2, and so on:
String name = "Nixon, Robin";
String formatted = name.replaceAll("(\\w+), (\\w+)", "$2 $1");
System.out.println(formatted); // Robin Nixon
matcher.group(n).
Flags and options
Regular expressions in Java can be compiled with flags that alter their behavior. You can combine flags using the bitwise OR (|) operator.
| Flag | Description |
Pattern.CASE_INSENSITIVE | Ignores case when matching |
Pattern.MULTILINE | Allows ^ and $ to match line boundaries |
Pattern.DOTALL | Makes . match newline characters too |
Pattern.UNICODE_CASE | Enables Unicode-aware case matching |
Pattern p = Pattern.compile("java", Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher("I love Java!");
System.out.println(m.find()); // true
Pattern.COMMENTS flag to keep them understandable.
Encoding, Unicode, and character sets
Modern software must handle text from many languages, scripts, and symbol systems. To do this reliably, Java uses the Unicode standard for all character data, ensuring that programs can represent and process text from any part of the world. Every char in Java and every character in a String is a Unicode value, not a raw byte. This design makes Java inherently portable and internationalized.
Unicode assigns a unique code point to every character, covering alphabets, ideograms, punctuation, emoji, and even control symbols. Code points are usually written in hexadecimal, prefixed with U+. For example, U+0041 represents the letter A, and U+1F600 represents the grinning face emoji 😀.
char letter = 'A'; // U+0041
char symbol = '\u03A9'; // Greek capital omega (Ω)
String emoji = "\uD83D\uDE00"; // 😀 (represented as a surrogate pair)
Java’s internal string representation uses UTF-16 encoding, meaning that most common characters occupy a single 16-bit char, while characters outside the Basic Multilingual Plane (BMP) use two 16-bit units known as a surrogate pair. This allows Java to represent the full range of Unicode characters transparently.
\uXXXX) in any Java source file. They are processed by the compiler before code is interpreted, making them useful for including special symbols or non-ASCII characters directly in code.
Character encoding and byte conversion
Although Java represents text internally as Unicode, data stored in files or transmitted over networks must eventually be converted into bytes. This conversion depends on a character encoding, such as UTF-8 or ISO-8859-1. The encoding determines how Unicode characters are mapped to byte sequences.
The String and java.nio.charset APIs provide methods to control this process explicitly. For example:
import java.nio.charset.StandardCharsets;
String text = "Café";
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
String decoded = new String(bytes, StandardCharsets.UTF_8);
System.out.println(decoded); // Café
This code converts a string to a UTF-8 byte array and back again safely. Using a fixed, explicit charset avoids platform-dependent behavior and ensures consistent results across systems.
UTF-8) to prevent data corruption or unreadable characters when running your program on different systems.
Working with character sets
The Charset class in java.nio.charset provides access to all available encodings supported by the Java runtime. You can query and list them or use them when reading and writing files.
import java.nio.charset.Charset;
for (String name : Charset.availableCharsets().keySet()) {
System.out.println(name);
}
Some of the most commonly used encodings are:
- UTF-8 — The dominant encoding for web and cross-platform text; variable length and efficient for ASCII-based data.
- UTF-16 — Java’s internal representation; fixed width for most characters, two units for extended symbols.
- ISO-8859-1 — Latin-1; a legacy single-byte encoding for Western European languages.
- US-ASCII — 7-bit encoding for basic English characters only.
UTF-8 is the safest and most portable choice for external file and network communication. It is fully compatible with ASCII and supports every Unicode character.
Unicode awareness in strings and characters
Because of UTF-16’s use of surrogate pairs, a single visual character may consist of more than one char. This distinction matters when iterating over strings or measuring their lengths.
String smile = "😀";
System.out.println(smile.length()); // 2 (two UTF-16 units)
System.out.println(smile.codePointCount(0, smile.length()));
// 1 (one Unicode code point)
To work safely with Unicode text, use the codePointAt() and codePoints() methods instead of direct character indexing. These handle surrogate pairs correctly and prevent cutting characters in half during iteration or substring operations.
smile.codePoints().forEach(cp ->
System.out.println(Integer.toHexString(cp))
);