Java Strings: Complete Guide to String Manipulation and Best Practices
Welcome back to the Java Fundamentals series! 👋
What is a String in Java?
A String in Java is an immutable sequence of characters that represents textual data. Here are the key characteristics:
- Immutable: Once created, the content cannot be modified
- Object type: Strings are objects of the
java.lang.String
class - Reference type: String variables store references to String objects
- UTF-16 encoding: Internally uses UTF-16 character encoding
- Thread-safe: Due to immutability, strings are inherently thread-safe
Key Point: Any operation like concat, replace, or substring returns a new String object rather than modifying the existing one.
String Declaration and Initialization
There are two main ways to create strings in Java:
// String literal (recommended)
String s1 = "Hello";
// Using new keyword (creates new object)
String s2 = new String("World");
String Pool vs Heap Memory
Understanding where strings are stored is crucial for performance:
- String literals → Stored in String Pool (part of heap memory)
new String()
→ Creates a new object directly in heap memory
String s1 = "Java"; // Stored in String Pool
String s2 = "Java"; // References same object in String Pool
String s3 = new String("Java"); // New object in heap
System.out.println(s1 == s2); // true (same reference)
System.out.println(s1 == s3); // false (different objects)
Memory Efficiency: The JVM reuses string literals for better memory utilization through the String Pool mechanism.
Essential String Methods for Problem Solving
Character and Length Operations
Method | Description | Example |
---|---|---|
charAt(int index) | Returns character at given index | "Java".charAt(1) → 'a' |
length() | Returns length of string | "Hello".length() → 5 |
Substring and Search Operations
Method | Description | Example |
---|---|---|
substring(int start, int end) | Extracts part of string [start, end-1] | "Hello".substring(1, 4) → "ell" |
indexOf(char/String) | Returns index of first occurrence | "Hello".indexOf('l') → 2 |
lastIndexOf(char/String) | Returns last occurrence index | "Hello".lastIndexOf('l') → 3 |
Comparison and Validation Methods
Method | Description | Example |
---|---|---|
equals(String) | Content comparison (case-sensitive) | "Java".equals("java") → false |
equalsIgnoreCase(String) | Content comparison (case-insensitive) | "Java".equalsIgnoreCase("java") → true |
contains(CharSequence) | Returns true if substring exists | "Hello World".contains("World") → true |
startsWith(String) | Checks prefix | "Hello".startsWith("He") → true |
endsWith(String) | Checks suffix | "Hello".endsWith("lo") → true |
Transformation Methods
Method | Description | Example |
---|---|---|
toUpperCase() / toLowerCase() | Case conversion | "Hello".toUpperCase() → "HELLO" |
replace(old, new) | Replace all occurrences | "Hello".replace("l", "x") → "Hexxo" |
split(regex) | Splits string by delimiter into array | "a,b,c".split(",") → ["a", "b", "c"] |
trim() | Removes leading & trailing whitespaces | " Hello ".trim() → "Hello" |
String Comparison: The Right Way
Always use .equals()
for content comparison, never ==
This is one of the most common mistakes in Java programming:
Operator | Compares | Use Case |
---|---|---|
== | Reference (object identity) | Check if two variables point to same object |
.equals() | Content | Check if two strings have same text content |
Example: Why ==
Can Be Misleading
String s1 = "abc"; // String pool
String s2 = new String("abc"); // Heap memory
System.out.println(s1 == s2); // false (different objects)
System.out.println(s1.equals(s2)); // true (same content)
Best Practices for String Comparison
// ✅ Good: Always use .equals()
if (userInput.equals("yes")) {
// Handle yes response
}
// ✅ Even better: Null-safe comparison
if ("yes".equals(userInput)) {
// Avoids NullPointerException if userInput is null
}
// ❌ Bad: Using == for content comparison
if (userInput == "yes") {
// This might not work as expected!
}
StringBuilder & StringBuffer: When and Why?
Since Strings are immutable, every modification creates a new String object. For heavy string manipulations, this becomes inefficient.
Performance Comparison
Class | Thread-Safe? | Performance | Best Use Case |
---|---|---|---|
StringBuilder | ❌ No | ⚡ Fastest | Single-threaded string building |
StringBuffer | ✅ Yes | 🐌 Slower | Multi-threaded environments |
String | ✅ Yes | 🐌 Slowest for modifications | Immutable text |
StringBuilder Example
StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // "Hello World"
sb.insert(5, ","); // "Hello, World"
sb.replace(7, 12, "Java"); // "Hello, Java"
System.out.println(sb.toString()); // "Hello, Java"
Why Not Just Use String?
Consider this inefficient approach:
// ❌ Inefficient: Creates multiple String objects
String result = "";
for (int i = 0; i < 1000; i++) {
result += "a"; // Creates new String object each iteration!
}
vs the efficient approach:
// ✅ Efficient: Modifies same StringBuilder object
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append("a"); // Modifies existing buffer
}
String result = sb.toString();
Key StringBuilder Methods
Essential methods for StringBuilder manipulation:
Method | Description | Example |
---|---|---|
append() | Add data to end | sb.append("text") |
insert() | Insert at given index | sb.insert(5, "new") |
replace() | Replace substring | sb.replace(0, 3, "Hi") |
delete() | Remove substring | sb.delete(2, 5) |
reverse() | Reverse entire content | sb.reverse() |
toString() | Convert back to String | sb.toString() |
Real-World Example: Building SQL Query
StringBuilder query = new StringBuilder("SELECT * FROM users");
if (name != null) {
query.append(" WHERE name = '").append(name).append("'");
}
if (age > 0) {
query.append(query.indexOf("WHERE") > 0 ? " AND" : " WHERE");
query.append(" age = ").append(age);
}
String finalQuery = query.toString();
Remember: StringBuilder is mutable — all changes happen to the same object, making it memory efficient.
Common String Patterns for Problem Solving
Here are some frequently used string manipulation patterns:
1. Checking for Palindromes
public boolean isPalindrome(String s) {
String cleaned = s.toLowerCase().replaceAll("[^a-z0-9]", "");
StringBuilder sb = new StringBuilder(cleaned);
return cleaned.equals(sb.reverse().toString());
}
2. Counting Character Frequency
public Map<Character, Integer> countChars(String s) {
Map<Character, Integer> freq = new HashMap<>();
for (char c : s.toCharArray()) {
freq.put(c, freq.getOrDefault(c, 0) + 1);
}
return freq;
}
3. String Rotation Check
public boolean isRotation(String s1, String s2) {
return s1.length() == s2.length() &&
(s1 + s1).contains(s2);
}
Performance Tips and Best Practices
1. Choose the Right Tool
- String: For immutable text and simple operations
- StringBuilder: For single-threaded string building
- StringBuffer: Only when thread safety is required
2. Avoid String Concatenation in Loops
// ❌ Avoid
String result = "";
for (String item : items) {
result += item; // Creates new String each time
}
// ✅ Prefer
StringBuilder sb = new StringBuilder();
for (String item : items) {
sb.append(item);
}
String result = sb.toString();
3. Use String Literals When Possible
// ✅ Good: Uses String Pool
String greeting = "Hello";
// ❌ Unnecessary: Creates new object
String greeting = new String("Hello");
4. Null Safety
// ✅ Null-safe comparison
if ("expected".equals(userInput)) { /* safe */ }
// ✅ Or check for null first
if (userInput != null && userInput.equals("expected")) { /* safe */ }
Conclusion
- Strings are immutable → Any modification creates a new object
- Use
.equals()
for content comparison, never==
- StringBuilder/StringBuffer → For heavy string manipulations
- String Pool → Helps with memory efficiency for literals
- Performance matters → Choose the right tool for the job
Happy coding! 💻