Strings in Java
Deep dive into Java String — immutability, common methods, StringBuilder, StringBuffer, string formatting, and regular expressions.
String is one of the most-used classes in Java. Understanding how it works — especially immutability and the string pool — prevents subtle bugs and performance issues that are otherwise very hard to diagnose.
String Basics
There are two ways to create a String. String literals are stored in the string pool — Java reuses the same object for identical literals, saving memory. Using new String(...) bypasses the pool and always creates a fresh object, which is almost never what you want.
String s1 = "Hello"; // string literal — stored in pool
String s2 = new String("Hello"); // new object — bypasses pool
String empty = ""; // empty string, length 0
String blank = " "; // not empty, but blank
Immutability
Every string operation that looks like a modification creates a new String — the original is left unchanged. This is not a flaw: it is what makes strings safe to pass to methods, use as map keys, and share across threads. The practical consequence is that you must reassign the variable to capture the result.
String s = "hello";
s.toUpperCase(); // returns a new String — does NOT modify s
System.out.println(s); // still "hello"
s = s.toUpperCase(); // reassign to capture the new String
System.out.println(s); // "HELLO"
== vs .equals()
== compares object identity (are these the same object in memory?), not content. For strings, always use .equals() to compare content. Relying on == produces correct results with pooled literals but fails silently with new String(...) or strings built at runtime.
String a = "java";
String b = "java";
String c = new String("java");
System.out.println(a == b); // true — same pool object
System.out.println(a == c); // false — c is a different object
System.out.println(a.equals(c)); // true — same content (always use this)
System.out.println(a.equalsIgnoreCase("JAVA")); // true
Always use .equals() to compare string content.
Common String Methods
These are the methods you will reach for most often. Every method returns a new String — remember to capture the result if you need it.
String s = " Hello, Java World! ";
// Length and characters
s.length() // 22
s.charAt(7) // 'J'
s.indexOf("Java") // 8
s.lastIndexOf("o") // 16
s.isEmpty() // false
s.isBlank() // false (Java 11+)
// Case and whitespace
s.trim() // "Hello, Java World!" (removes ASCII whitespace)
s.strip() // "Hello, Java World!" (Unicode-aware, Java 11+)
s.toLowerCase() // " hello, java world! "
s.toUpperCase() // " HELLO, JAVA WORLD! "
// Testing content
s.contains("Java") // true
s.startsWith(" Hello") // true
s.endsWith("! ") // true
// Extracting substrings
s.substring(8, 12) // "Java"
s.substring(8) // "Java World! "
// Replacing
s.replace("Java", "Python") // " Hello, Python World! "
s.replaceAll("\\s+", "-") // "--Hello,-Java-World!--"
s.replaceFirst("[A-Z]", "X") // " Xello, Java World! "
// Splitting
"a,b,c".split(",") // ["a", "b", "c"]
"one two three".split("\\s+") // ["one", "two", "three"]
// Joining (Java 8+)
String.join(", ", "Alice", "Bob", "Charlie") // "Alice, Bob, Charlie"
String.join("-", List.of("2024", "01", "15")) // "2024-01-15"
String Comparison Methods
compareTo is used for sorting — it returns a negative number, zero, or positive number indicating the lexicographic order. Collections and sorted data structures use this internally when you sort strings.
"apple".compareTo("banana") // negative (a < b)
"banana".compareTo("apple") // positive
"apple".compareTo("apple") // 0 (equal)
"Apple".compareToIgnoreCase("apple") // 0
String Formatting
String formatting lets you build readable output without messy concatenation chains. The format specifiers match the type of the value being inserted and control width, precision, and alignment.
// String.format — like printf but returns a String
String msg = String.format("Name: %s, Age: %d, Score: %.2f", "Alice", 25, 98.5);
// "Name: Alice, Age: 25, Score: 98.50"
// Common format specifiers
// %s — String
// %d — integer (int, long)
// %f — floating-point (%,.2f for comma separator + 2 decimal places)
// %n — newline (platform-safe)
// %b — boolean
// %c — char
// %x — hexadecimal
// printf — formats and prints directly
System.out.printf("%-15s %5d%n", "Alice", 95); // left-align name, right-align score
// Text blocks (Java 15+) — multi-line strings with clean indentation
String json = """
{
"name": "Alice",
"age": 25,
"active": true
}
""";
StringBuilder — Efficient String Building
String concatenation in a loop creates a new object on every iteration — for 1000 iterations that is 1000 throwaway String objects that the garbage collector then has to clean up. StringBuilder solves this by maintaining a single mutable character buffer and producing one String only at the end.
// Bad — creates 1000 String objects
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // new String each time
}
// Good — single mutable buffer, far less garbage
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i);
}
String result = sb.toString(); // one final String
StringBuilder Methods
StringBuilder sb = new StringBuilder("Hello");
sb.append(", World"); // "Hello, World"
sb.insert(5, " there"); // "Hello there, World"
sb.delete(5, 11); // "Hello, World"
sb.replace(7, 12, "Java"); // "Hello, Java"
sb.reverse(); // "avaJ ,olleH"
sb.reverse(); // "Hello, Java" (reversed back)
sb.length() // 11
sb.charAt(0) // 'H'
sb.indexOf("Java") // 7
sb.toString() // "Hello, Java"
// Method chaining — each mutating method returns the same StringBuilder
String csv = new StringBuilder()
.append("Alice")
.append(",")
.append(30)
.append(",")
.append("Engineer")
.toString(); // "Alice,30,Engineer"
StringBuffer
StringBuffer has the same API as StringBuilder but all methods are synchronized — meaning it is safe for multiple threads to use the same instance concurrently. In practice, string building is almost always local to a single method, so prefer StringBuilder unless you have a concrete threading requirement.
StringBuffer buf = new StringBuffer();
buf.append("thread-safe");
buf.insert(0, "This is ");
System.out.println(buf.toString()); // "This is thread-safe"
Regular Expressions
Regular expressions let you describe text patterns rather than literal strings. They are the right tool for validation (does this look like an email?), extraction (pull all phone numbers from a document), and complex replacement. Java uses the java.util.regex package, and many String methods accept regex patterns directly.
import java.util.regex.*;
String email = "[email protected]";
// matches() — entire string must match the pattern
boolean valid = email.matches("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");
System.out.println(valid); // true
// String methods that accept regex
"hello world".replaceAll("[aeiou]", "*") // "h*ll* w*rld"
"one1two2three3".split("\\d") // ["one", "two", "three"]
"2024-01-15".matches("\\d{4}-\\d{2}-\\d{2}") // true
Common Regex Patterns
| Pattern | Matches |
|---|---|
\\d | Any digit (0–9) |
\\w | Word character (letter, digit, _) |
\\s | Whitespace |
. | Any character |
[abc] | a, b, or c |
[^abc] | Not a, b, or c |
[a-z] | Lowercase letter |
a+ | One or more a |
a* | Zero or more a |
a? | Zero or one a |
a{3} | Exactly three a |
^ | Start of string |
$ | End of string |
Pattern and Matcher — for Repeated Use
Compiling a regex into a Pattern once and reusing it is significantly faster than calling String.matches() in a loop, because matches() recompiles the pattern every call.
Pattern phonePattern = Pattern.compile("\\(?(\\d{3})\\)?[-.\\s]?(\\d{3})[-.\\s]?(\\d{4})");
String[] inputs = {"(555) 123-4567", "555.123.4567", "invalid"};
for (String input : inputs) {
Matcher m = phonePattern.matcher(input);
if (m.matches()) {
System.out.printf("Area: %s, Number: %s-%s%n", m.group(1), m.group(2), m.group(3));
} else {
System.out.println("Not a valid phone: " + input);
}
}
Projects
Password Validator
public class PasswordValidator {
public static boolean isValid(String password) {
if (password.length() < 8) return false;
if (!password.matches(".*[A-Z].*")) return false; // at least one uppercase
if (!password.matches(".*[a-z].*")) return false; // at least one lowercase
if (!password.matches(".*\\d.*")) return false; // at least one digit
if (!password.matches(".*[!@#$%^&*].*")) return false; // at least one special char
return true;
}
public static String feedback(String password) {
StringBuilder issues = new StringBuilder();
if (password.length() < 8) issues.append("At least 8 characters. ");
if (!password.matches(".*[A-Z].*")) issues.append("Add an uppercase letter. ");
if (!password.matches(".*[a-z].*")) issues.append("Add a lowercase letter. ");
if (!password.matches(".*\\d.*")) issues.append("Add a digit. ");
if (!password.matches(".*[!@#$%^&*].*")) issues.append("Add a special character (!@#$%^&*). ");
return issues.length() == 0 ? "Strong password!" : issues.toString().trim();
}
public static void main(String[] args) {
String[] passwords = {"pass", "Password1", "P@ssword1"};
for (String p : passwords) {
System.out.printf("%-15s → %s%n", p, feedback(p));
}
}
}
Text Analyzer
public class TextAnalyzer {
public static void main(String[] args) {
String text = "The quick brown fox jumps over the lazy dog. " +
"Pack my box with five dozen liquor jugs.";
String[] words = text.toLowerCase().split("[^a-z]+");
int wordCount = words.length;
int charCount = text.replaceAll("\\s", "").length();
int sentenceCount = text.split("[.!?]+").length;
System.out.println("Words: " + wordCount);
System.out.println("Chars: " + charCount);
System.out.println("Sentences: " + sentenceCount);
// Count vowels using the chars() stream
long vowels = text.chars()
.filter(c -> "aeiouAEIOU".indexOf(c) >= 0)
.count();
System.out.println("Vowels: " + vowels);
// Most frequent word using a frequency map
java.util.Map<String, Integer> freq = new java.util.HashMap<>();
for (String w : words) {
if (!w.isEmpty()) freq.merge(w, 1, Integer::sum);
}
String topWord = freq.entrySet().stream()
.max(java.util.Map.Entry.comparingByValue())
.map(java.util.Map.Entry::getKey)
.orElse("");
System.out.println("Top word: \"" + topWord + "\" (" + freq.get(topWord) + "x)");
}
}