Strings and Text Processing
The String Class
Section titled “The String Class”String is the most used class in the Java platform. It is finalImplements Serializable Comparable<String>And CharSequenceAnd its instances are immutable. Every character in a String is stored internally as UTF-16 code units in a byte[] (since JDK 9, compact strings use byte[] with a coder flag for LATIN1 vs UTF-16).
Immutability
Section titled “Immutability”Once constructed, a String object cannot be modified. Every “mutating” operation — substring concat``replace``toUpperCase``trim — returns a new String object. The original remains Unchanged. This is not a suggestion; the String class has no public mutating methods, and the Backing byte[] (the value field) is private.
String original = "hello";String upper = original.toUpperCase();System.out.println(original); // "hello" — unchangedSystem.out.println(upper); // "HELLO"System.out.println(original == upper); // falseImmutability is enforced by design:
- All fields are
private final. - No method on
Stringmodifies internal state. - The class is
final— you cannot subclass it to introduce mutability.
Why immutable? Thread safety without synchronization, safe sharing across threads, secure use as HashMap keys and HashSet elements (hash code is cached at construction and never changes), and String literal interning.
String Pool (Intern Pool)
Section titled “String Pool (Intern Pool)”The JVM maintains a pool of unique String literals. When the bytecode contains a string literal, The JVM looks up the pool; if the literal already exists, it reuses the reference. This means two Variables assigned the same literal may be the same object at runtime:
String a = "hello";String b = "hello";System.out.println(a == b); // true — same object from the pool
String c = new String("hello");System.out.println(a == c); // false — new object on the heapSystem.out.println(a.equals(c)); // true — same contentThe new String(...) constructor always creates a new object on the heap, bypassing the pool. This Is almost always the wrong thing to do.
intern()
Section titled “intern()”String.intern() returns a canonical representation from the pool. If the string is not already in The pool, it is added. Interning can reduce memory when you have many duplicate strings, but the Pool lives in the heap (since JDK 7) and is managed by the GC. Over-interning can cause GC pressure And pool bloat.
String s1 = new String("hello").intern();String s2 = "hello";System.out.println(s1 == s2); // trueBefore JDK 9, every String stored its characters in a char[] — 2 bytes per character. JDK 9 Introduced compact strings (-XX:+CompactStringsEnabled by default since JDK 9). If all Characters fit in the LATIN1 range (code points 0-255), the string uses a byte[] with 1 byte per Character. If any character exceeds LATIN1, it switches to UTF-16 encoding (2 bytes per character). This reduces memory usage by roughly 50% for most real-world strings.
// LATIN1 encoding — 1 byte per charString latin1 = "Hello, World!";// UTF-16 encoding — 2 bytes per char (contains 世, U+4E16, outside LATIN1 range)String utf16 = "世界";The coder flag is stored in the coder field of the String object. You cannot control which Encoding is used; it is determined automatically at construction time.
String vs StringBuilder vs StringBuffer
Section titled “String vs StringBuilder vs StringBuffer”Performance Characteristics
Section titled “Performance Characteristics”| Operation | String | StringBuilder | StringBuffer |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread safety | N/A (immutable) | Not thread-safe | Synchronized (thread-safe) |
| Append performance | O(n) per append (copies array) | Amortized O(1) | Amortized O(1) + sync cost |
| Memory overhead | New object per operation | Single buffer, resizes as needed | Single buffer + sync overhead |
When to Use Each
Section titled “When to Use Each”String — Use for values that do not change. Literals, constants, method return values for Immutable data, keys in maps, and any case where immutability is desired. The JVM”s escape analysis And JIT can sometimes optimize string concatenation into StringBuilder automatically.
StringBuilder — Use for building strings in a single thread. This covers the vast majority of Use cases: constructing SQL queries, building JSON, accumulating log messages, reading file Contents.
StringBuffer — Use only when multiple threads need to append to the same buffer concurrently. This is rare. In practice, you almost always want StringBuilder and handle thread safety at a Higher level.
// BAD — creates many intermediate String objectsString result = "";for (int i = 0; i < 1000; i++) { result += "item" + i + ",";}// Each += creates a new StringBuilder, appends, and creates a new String
// GOOD — single StringBuilderStringBuilder sb = new StringBuilder(10000); // pre-size if you know the approximate lengthfor (int i = 0; i < 1000; i++) { sb.append("item").append(i).append(',');}String result = sb.toString();Concatenation Under the Hood
Section titled “Concatenation Under the Hood”The Java compiler translates string concatenation with the + operator into StringBuilder Operations at compile time (JLS §15.18.1). However, this optimization only applies within a single Expression:
// Single expression — compiler optimizes to one StringBuilderString s = a + b + c + d;
// Loop — each iteration creates a new StringBuilder (pre-JDK 9)// JDK 9+ uses invokedynamic with StringConcatFactory for better performanceString s = "";for (String part : parts) { s += part; // pre-JDK 9: new StringBuilder each iteration}JDK 9+ uses invokedynamic with StringConcatFactory for string concatenation, which generates Optimized bytecode at runtime. This can outperform the StringBuilder approach , Especially for concatenations involving non-string types.
Text Blocks (JDK 15+)
Section titled “Text Blocks (JDK 15+)”Text blocks, standardized in JDK 15 (JEP 378), provide a way to write multi-line strings without Escape sequences. They are delimited by triple double quotes:
String html = """ <html> <body> <p>Hello, %s</p> </body> </html> """.formatted(name);
String json = """ { "name": "%s", "age": %d } """.stripIndent();Key Rules
Section titled “Key Rules”- The opening
"""must be followed by a line terminator (the content starts on the next line). - The closing
"""can be on its own line or at the end of the last content line. - Incidental white space is determined by the position of the closing
""". - Two trailing spaces on a line are preserved (otherwise trailing spaces are stripped).
// Incidental whitespace removalString query = """ SELECT id, name, email FROM users WHERE status = 'ACTIVE' ORDER BY name """;// Equivalent to: "SELECT id, name, email\nFROM users\nWHERE status = 'ACTIVE'\nORDER BY name\n"Escaping in Text Blocks
Section titled “Escaping in Text Blocks”Most escape sequences work normally. The \ at the end of a line prevents a line break, and \s Produces a single space (useful for preserving trailing whitespace):
String text = """ This is a single \ line because the backslash prevents the line break.\ """;// Result: "This is a single line because the backslash prevents the line break."Core String Methods
Section titled “Core String Methods”Substring
Section titled “Substring”String s = "Hello, World!";String sub = s.substring(7, 12); // "World"String csv = "one,two,three,four";String[] parts = csv.split(",");// parts: ["one", "two", "three", "four"]
// split with limitString[] limited = csv.split(",", 3);// limited: ["one", "two", "three,four"]
// join (JDK 8+)String joined = String.join(", ", parts);// joined: "one, two, three, four"split(String regex) compiles the regex pattern every call. If you split the same pattern Repeatedly in a hot path, compile the Pattern once and reuse it:
private static final Pattern COMMA = Pattern.compile(",");
String[] parts = COMMA.split(csv);Strip, Trim, and Whitespace
Section titled “Strip, Trim, and Whitespace”String s = " Hello, World! ";s.trim(); // "Hello, World!" — removes ASCII whitespace (<= U+0020)s.strip(); // "Hello, World!" — removes Unicode whitespace (JDK 11+)s.stripLeading(); // "Hello, World! "s.stripTrailing(); // " Hello, World!"s.stripIndent(); // removes incidental indentation (JDK 15+)Replace
Section titled “Replace”String s = "Hello, World!";s.replace("World", "Java"); // "Hello, Java!"s.replaceAll("\\d+", "NUM"); // regex-based replacements.replaceFirst("\\d+", "NUM"); // replace first match onlys.chars() .filter(Character::isDigit) .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) .toString();Format
Section titled “Format”String.format("User %s has %d points (balance: %.2f)", name, points, balance);
// Positional argumentsString.format("%2$s is %1$d years old", age, name);The Character Class
Section titled “The Character Class”Character wraps a single char value. It provides static utility methods for classification and Conversion:
// ClassificationCharacter.isDigit('5'); // trueCharacter.isLetter('A'); // trueCharacter.isUpperCase('a'); // falseCharacter.isWhitespace(' '); // trueCharacter.isDefined('\u20AC');// true (€)
// Conversionchar upper = Character.toUpperCase('a'); // 'A'int codePoint = Character.codePointAt("€", 0); // 8364char[] chars = Character.toChars(0x1F600); // 😀 (surrogate pair)Code Points vs char
Section titled “Code Points vs char”Java’s char is a UTF-16 code unit (16 bits). Characters outside the Basic Multilingual Plane (BMP) — emoji, rare CJK characters, mathematical symbols — require surrogate pairs (two char Values). Working with char directly on such strings will produce incorrect results. Use code point APIs instead:
String emoji = "Hello 🌍";System.out.println(emoji.length()); // 8 (surrogate pair counts as 2)System.out.println(emoji.codePointCount(0, emoji.length())); // 7
// Iterate by code pointemoji.codePoints().forEach(cp -> { System.out.printf("U+%04X %n", cp);});// U+0048 U+0065 U+006C U+006C U+006F U+0020 U+1F30DJava’s regex engine is in java.util.regex. The two primary classes are Pattern (compiled Representation) and Matcher (stateful engine that performs match operations against input).
Pattern and Matcher
Section titled “Pattern and Matcher”import java.util.regex.Pattern;import java.util.regex.Matcher;
Pattern pattern = Pattern.compile("\\b(\\d{3})[-.]?(\\d{3})[-.]?(\\d{4})\\b");Matcher matcher = pattern.matcher("Call 555-123-4567 or 555.987.6543");
while (matcher.find()) { String areaCode = matcher.group(1); String exchange = matcher.group(2); String subscriber = matcher.group(3); System.out.printf("Phone: (%s) %s-%s%n", areaCode, exchange, subscriber);}// Phone: (555) 123-4567// Phone: (555) 987-6543Predefined Character Classes
Section titled “Predefined Character Classes”| Expression | Meaning |
|---|---|
. | Any character (except line terminators) |
\d | Digit [0-9] |
\D | Non-digit |
\s | Whitespace |
\S | Non-whitespace |
\w | Word character [a-zA-Z_0-9] |
\W | Non-word character |
Quantifiers
Section titled “Quantifiers”| Quantifier | Greedy | Reluctant | Possessive | Meaning |
|---|---|---|---|---|
| Zero or one | X? | X?? | X?+ | |
| Zero or more | X* | X*? | X*+ | |
| One or more | X+ | X+? | X++ | |
| Exactly n | X{n} | X{n}? | X{n}+ | |
| n to m | X{n,m} | X{n,m}? | X{n,m}+ |
Common Patterns
Section titled “Common Patterns”// Email (basic, not RFC 5322 compliant)Pattern EMAIL = Pattern.compile("[\\w.+-]+@[\\w-]+\\.[\\w.]+");
// IPv4 addressPattern IPV4 = Pattern.compile( "(?:(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d\\d?)");
// Named groups (JDK 7+)Pattern LOG = Pattern.compile( "(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) " + "(?<level>\\w+) " + "(?<message>.*)");
Matcher m = LOG.matcher("2024-01-15 10:30:00 ERROR Connection timeout");if (m.matches()) { System.out.println(m.group("level")); // "ERROR" System.out.println(m.group("message")); // "Connection timeout"}Performance Note
Section titled “Performance Note”Compiling a Pattern is expensive. Always compile once and reuse:
// BAD — compiles on every callpublic boolean isValidEmail(String input) { return input.matches("[\\w.+-]+@[\\w-]+\\.[\\w.]+"); // String.matches() compiles and discards the pattern every time}
// GOOD — compile onceprivate static final Pattern EMAIL_PATTERN = Pattern.compile("[\\w.+-]+@[\\w-]+\\.[\\w.]+");
public boolean isValidEmail(String input) { return EMAIL_PATTERN.matcher(input).matches();}Character Encoding
Section titled “Character Encoding”UTF-8, UTF-16, and ISO-8859-1
Section titled “UTF-8, UTF-16, and ISO-8859-1”| Encoding | Variable width | Bytes per char (typical) | Notes |
|---|---|---|---|
| UTF-8 | Yes | 1-4 | Dominant on the web, wire |
| UTF-16 | Yes | 2-4 | Java internal representation |
| UTF-32 | No | 4 | Fixed width, memory-heavy |
| ISO-8859-1 | No | 1 | Latin-1, lossy for non-Latin |
| ASCII | No | 1 | Subset of UTF-8 and Latin-1 |
Java’s String stores text as UTF-16 code units internally. When converting to/from bytes (for I/O, Network, storage), you must specify the charset.
Encoding and Decoding
Section titled “Encoding and Decoding”// Always specify the charset explicitlybyte[] utf8Bytes = "Hello".getBytes(StandardCharsets.UTF_8);String decoded = new String(utf8Bytes, StandardCharsets.UTF_8);
// Using Charset directlyCharset charset = Charset.forName("UTF-8");// Better: use the constantCharset charset2 = StandardCharsets.UTF_8;
// List available charsetsSortedMap<String, Charset> available = Charset.availableCharsets();Encoding Pitfalls
Section titled “Encoding Pitfalls”// PITFALL: using platform default charsetbyte[] bytes = "Hello".getBytes(); // uses platform default — non-portableString s = new String(bytes); // same problem
// PITFALL: silent replacement of unencodable charactersCharsetEncoder encoder = StandardCharsets.ISO_8859_1.newEncoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE);// "€" becomes "?" in ISO-8859-1 — data loss with no error
// SAFE: fail on encoding errorsCharsetEncoder strict = StandardCharsets.UTF_8.newEncoder() .onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT);String.format
Section titled “String.format”String.format uses Formatter internally and supports format specifiers similar to printf in C:
String s = String.format("Name: %s, Age: %d, Balance: $%,.2f", "Alice", 30, 12345.678);// "Name: Alice, Age: 30, Balance: $12,345.68"| Specifier | Meaning | Example |
|---|---|---|
%s | String | "hello" |
%d | Decimal integer | 42 |
%f | Decimal floating point | 3.141593 |
%,d | Decimal with comma separator | 1,234,567 |
%x | Hexadecimal | ff |
%o | Octal | 377 |
%b | Boolean | true |
%c | Character | A |
%n | Platform-specific line separator | |
%% | Literal percent | % |
%20s | Right-pad string to width 20 | " hello" |
%-20s | Left-pad string to width 20 | "hello " |
%05d | Zero-pad integer to width 5 | 00042 |
MessageFormat
Section titled “MessageFormat”MessageFormat is useful for localization because it supports positional arguments and ChoiceFormat:
String pattern = "On {0, date, long}, {1} found {2, choice, 0#no files|1#one file|1<{2} files}.";String result = MessageFormat.format(pattern, new Date(), "Alice", 3);// "On January 15, 2024, Alice found 3 files."formatted Method (JDK 15+)
Section titled “formatted Method (JDK 15+)”// Instance method on String — cleaner syntaxString template = "Hello, %s! You have %d new messages.";String result = template.formatted("Alice", 5);StringTokenizer (Legacy)
Section titled “StringTokenizer (Legacy)”StringTokenizer predates String.split() and Pattern. It is retained for backward compatibility But should not be used in new code. It does not support regex, cannot handle empty tokens, and has No way to limit splits.
// LEGACY — do not useStringTokenizer st = new StringTokenizer("one,two,three", ",");while (st.hasMoreTokens()) { System.out.println(st.nextToken());}
// MODERN — use split or PatternString[] parts = "one,two,three".split(",");