Strings in Java
1. What is a String?
A String in Java is a sequence of characters. It is
an object of the java.lang.String class,
which is automatically imported in every Java program. Strings are
one of the most frequently used types in Java and have special
language-level support.
Three key characteristics of Java Strings:
- Immutable: Once a String object is created, its content cannot be changed. Any operation that seems to modify a String actually creates a new one.
- String Pool: Java maintains a special area of heap memory called the String Constant Pool. When you create a string literal, Java checks the pool first; if the value already exists, it reuses that object instead of creating a new one.
-
Object vs Literal: Strings can be created as
literals (stored in the pool) or using the
newkeyword (always creates a new heap object, bypassing the pool).
// String as an object in memory: // // String Pool (inside heap): // +------------------+ // | "Hello" <------+--- s1 (literal) // | |+-- s2 (literal — same object reused!) // +------------------+ // // Regular Heap: // +------------------+ // | "Hello" <------+--- s3 (new keyword — different object) // +------------------+
2. Creating Strings
String Literal (Preferred)
String s1 = "Hello"; String s2 = "Hello"; // reuses the same pool object as s1 System.out.println(s1 == s2); // true (same reference in pool) System.out.println(s1.equals(s2)); // true (same content)
true
Using the new Keyword
String s3 = new String("Hello");
String s4 = new String("Hello");
System.out.println(s3 == s4); // false (different heap objects)
System.out.println(s3.equals(s4)); // true (same content)
true
Other Ways to Create Strings
// From a char array
char[] chars = {'J', 'a', 'v', 'a'};
String fromChars = new String(chars);
System.out.println(fromChars); // Java
// From an integer / other type
String fromInt = String.valueOf(42);
String fromDouble = String.valueOf(3.14);
System.out.println(fromInt + " " + fromDouble); // 42 3.14
42 3.14
Best Practice: Always use string literals instead
of new String(...). Literals benefit
from pool sharing, reducing memory usage. Reserve
new String() for rare cases where you
explicitly need a distinct heap object.
String Constant Pool (SCP)
The String Constant Pool (SCP) is a special area
managed by the JVM for reusing string literals. A literal such as
"Java" is checked in the pool before
a new object is created. If the same value already exists, Java
reuses the existing object.
String first = "Java";
String second = "Java";
String third = new String("Java");
first == second; // true: both references point to the SCP object
first == third; // false: third points to a separate heap object
// Add a runtime string to the SCP when appropriate:
String pooled = new String("Spring").intern();
JVM heap
+-------------------------------------------+
| String Constant Pool (SCP) |
| +---------+ |
| | "Java" | <----- first, second |
| +---------+ |
| |
| Ordinary heap |
| +---------+ |
| | "Java" | <----- third |
| +---------+ |
+-------------------------------------------+
Remember: the SCP is about sharing literal objects; it is not a separate physical memory area outside the heap. The exact internal layout is JVM-specific, but thinking of it as a shared pool is the easiest way to understand it.
How to Create an Immutable Class
An immutable object cannot change after construction. Every method
that appears to update it must return a new object. Java's
String class follows this design.
Follow these beginner-friendly rules:
- Declare the class
final. - Make all fields
private final. - Set fields only through the constructor.
- Do not provide setters or methods that mutate state.
- Make defensive copies of mutable input and output objects.
public final class UserProfile {
private final String username;
private final List<String> roles;
public UserProfile(String username, List<String> roles) {
this.username = username;
this.roles = List.copyOf(roles); // defensive copy
}
public String getUsername() {
return username;
}
public List<String> getRoles() {
return roles; // List.copyOf made it unmodifiable
}
}
UserProfile profile = new UserProfile("anand", List.of("ADMIN"));
// profile.username = "other"; // not allowed: no setter
// profile.getRoles().add("USER"); // UnsupportedOperationException
Why String is safer: a String has no setter and its internal character data is never exposed for modification. This makes it safe to share between methods, threads, caches, and security checks.
3. Why Strings are Immutable
Java designers made the String class
immutable for three important reasons:
1. Memory Efficiency (String Pool)
Because strings are immutable, the JVM can safely share a single instance of a string value across many references. If strings were mutable, one reference changing the content would corrupt all other references pointing to the same pool entry.
String a = "Java"; String b = "Java"; // Both 'a' and 'b' safely point to the same pool object. // No risk of corruption because neither can change it.
2. Thread Safety
Immutable objects are inherently thread-safe. Multiple threads can
read the same String simultaneously
without synchronization, because no thread can alter its state.
// Safe to share across threads — no synchronization needed String config = "database=localhost;port=5432"; // Thread 1, Thread 2, Thread 3 all read 'config' safely
3. Security
Strings are used for sensitive data such as file paths, network URLs, database connection strings, and passwords. If strings were mutable, malicious code could alter the value after a security check but before the actual use.
// Hypothetical security issue if String were mutable: String filename = "/secure/data.txt"; // ... security check passes on "/secure/data.txt" ... // ... if mutable, attacker mutates to "/etc/passwd" ... // ... file is opened with attacker's path — security breach! // Because String is immutable, this attack is impossible.
Practical impact: The immutability of String
means any operation like
toUpperCase() or
replace() returns a
new String. The original is never altered. Always capture
the result: s = s.toUpperCase();
4. String Comparison: .equals() vs ==
This is one of the most common sources of bugs for Java beginners.
The == operator compares
references (memory addresses), not content. The
.equals() method compares the actual
character sequence.
public class StringComparison {
public static void main(String[] args) {
String a = "Hello";
String b = "Hello";
String c = new String("Hello");
String d = new String("Hello");
// Reference comparison with ==
System.out.println(a == b); // true — same pool object
System.out.println(a == c); // false — pool vs heap object
System.out.println(c == d); // false — two different heap objects
// Content comparison with .equals()
System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // true
System.out.println(c.equals(d)); // true
// Case-insensitive comparison
String x = "java";
String y = "JAVA";
System.out.println(x.equalsIgnoreCase(y)); // true
}
}
false
false
true
true
true
true
Golden Rule: ALWAYS use
.equals() to compare String content.
Never use == for strings. For
null-safe comparison, use
Objects.equals(a, b) from
java.util.Objects.
5. String Concatenation
The + Operator
The simplest way to join strings. Java overloads the
+ operator for String concatenation. Any
primitive or object concatenated with a String is automatically
converted to its String representation.
String first = "Hello";
String second = "World";
String result = first + ", " + second + "!";
System.out.println(result); // Hello, World!
int age = 25;
System.out.println("Age: " + age); // Age: 25 (int auto-converted)
Age: 25
The concat() Method
String s1 = "Good ";
String s2 = s1.concat("Morning");
System.out.println(s2); // Good Morning
// s1 is unchanged: "Good "
StringBuilder for Loops (Best Practice)
Using + inside a loop creates many
intermediate String objects, wasting memory. Use
StringBuilder for efficient repeated
concatenation.
// Inefficient — creates many temporary String objects
String result = "";
for (int i = 1; i <= 5; i++) {
result += i + " "; // each iteration creates a new String
}
System.out.println(result);
// Efficient — StringBuilder modifies a single buffer
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 5; i++) {
sb.append(i).append(" ");
}
System.out.println(sb.toString());
1 2 3 4 5
6. Important String Methods
The String class provides a rich set of
methods. All methods return new Strings without modifying the
original (immutability).
| Method | Syntax / Example | Output |
|---|---|---|
length() |
"Hello".length() |
5 |
charAt(i) |
"Java".charAt(1) |
'a' |
substring(start) |
"Tutorial".substring(4) |
"rial" |
substring(start, end) |
"Tutorial".substring(0, 4) |
"Tuto" |
indexOf(str) |
"banana".indexOf("an") |
1 |
lastIndexOf(str) |
"banana".lastIndexOf("an") |
3 |
toUpperCase() |
"hello".toUpperCase() |
"HELLO" |
toLowerCase() |
"WORLD".toLowerCase() |
"world" |
trim() |
" hi ".trim() |
"hi" |
strip() |
" hi ".strip() |
"hi" (Unicode-aware, Java 11+)
|
replace(old, new) |
"aabbcc".replace("bb","XX") |
"aaXXcc" |
contains(seq) |
"Java".contains("av") |
true |
startsWith(str) |
"Hello".startsWith("He") |
true |
endsWith(str) |
"Hello".endsWith("lo") |
true |
split(regex) |
"a,b,c".split(",") |
["a","b","c"] |
isEmpty() |
"".isEmpty() |
true |
isBlank() |
" ".isBlank() |
true (Java 11+) |
toCharArray() |
"Hi".toCharArray() |
['H','i'] |
compareTo(str) |
"apple".compareTo("banana") |
negative int |
public class StringMethodsDemo {
public static void main(String[] args) {
String s = " Hello, Java World! ";
System.out.println(s.trim()); // Hello, Java World!
System.out.println(s.trim().length()); // 19
System.out.println(s.trim().toUpperCase()); // HELLO, JAVA WORLD!
System.out.println(s.trim().replace("Java", "Amazing Java"));
System.out.println(s.trim().contains("Java")); // true
System.out.println(s.trim().startsWith("He")); // true
System.out.println(s.trim().charAt(7)); // J
System.out.println(s.trim().substring(7, 11)); // Java
System.out.println(s.trim().indexOf("World")); // 12
// Splitting a CSV string
String csv = "Alice,Bob,Charlie,Dave";
String[] names = csv.split(",");
for (String name : names) {
System.out.print("[" + name + "] ");
}
}
}
19
HELLO, JAVA WORLD!
Hello, Amazing Java World!
true
true
J
Java
12
[Alice] [Bob] [Charlie] [Dave]
7. String vs StringBuffer vs StringBuilder
Java provides three classes for working with character sequences, each with different characteristics:
| Feature | String | StringBuffer | StringBuilder |
|---|---|---|---|
| Mutability | Immutable | Mutable | Mutable |
| Thread Safety | Yes (immutable) | Yes (synchronized) | No |
| Performance | Slow for concat in loops | Moderate | Fast |
| Storage | String Pool / Heap | Heap | Heap |
| Introduced in | JDK 1.0 | JDK 1.0 | JDK 1.5 |
| Use when | Value won't change | Multi-threaded modification | Single-threaded modification |
Rule of thumb: Use
String for fixed values,
StringBuilder for building strings in
a single thread, and StringBuffer when
multiple threads modify the same buffer.
8. StringBuffer
StringBuffer is a mutable sequence of
characters that is thread-safe because its methods
are synchronized. Use it when multiple threads share and modify the
same string buffer.
public class StringBufferDemo {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("Hello");
// append() — adds to the end
sb.append(", World");
System.out.println(sb); // Hello, World
// insert(index, str) — inserts at a position
sb.insert(5, " Beautiful");
System.out.println(sb); // Hello Beautiful, World
// replace(start, end, str)
sb.replace(6, 15, "Wonderful");
System.out.println(sb); // Hello Wonderful, World
// delete(start, end)
sb.delete(5, 16);
System.out.println(sb); // Hello World
// reverse()
sb.reverse();
System.out.println(sb); // dlroW olleH
// length() and capacity()
StringBuffer sb2 = new StringBuffer();
System.out.println("Length: " + sb2.length()); // 0
System.out.println("Capacity: " + sb2.capacity()); // 16 (default)
// charAt and setCharAt
StringBuffer sb3 = new StringBuffer("Java");
sb3.setCharAt(0, 'j');
System.out.println(sb3); // java
}
}
Hello Beautiful, World
Hello Wonderful, World
Hello World
dlroW olleH
Length: 0
Capacity: 16
java
9. StringBuilder
StringBuilder is identical to
StringBuffer in its API but is
not synchronized, making it faster for
single-threaded use cases. It is the preferred choice for building
strings in most Java programs.
public class StringBuilderDemo {
public static void main(String[] args) {
// Building a sentence word by word
StringBuilder sb = new StringBuilder();
sb.append("Learning")
.append(" ")
.append("Java")
.append(" ")
.append("is")
.append(" ")
.append("fun!");
System.out.println(sb.toString()); // Learning Java is fun!
// Efficiently building a numbered list
StringBuilder list = new StringBuilder();
String[] items = {"Apple", "Banana", "Cherry"};
for (int i = 0; i < items.length; i++) {
list.append(i + 1).append(". ").append(items[i]).append("\n");
}
System.out.print(list);
// Reversing a string using StringBuilder
String original = "racecar";
String reversed = new StringBuilder(original).reverse().toString();
System.out.println(original + " reversed = " + reversed);
// Performance comparison comment:
// Concatenating 10,000 strings with '+' is ~1000x slower
// than using StringBuilder.append() in a loop.
}
}
1. Apple
2. Banana
3. Cherry
racecar reversed = racecar
Common StringBuilder / StringBuffer Methods
| Method | Description | Example |
|---|---|---|
append(x) |
Appends x to the end | sb.append("Hi") |
insert(i, x) |
Inserts x at index i | sb.insert(2, "XY") |
delete(s, e) |
Deletes chars from s to e (exclusive) | sb.delete(1, 3) |
replace(s, e, str) |
Replaces chars s to e with str | sb.replace(0,2,"AB") |
reverse() |
Reverses the character sequence | sb.reverse() |
charAt(i) |
Returns char at index i | sb.charAt(0) |
setCharAt(i, c) |
Sets char at index i to c | sb.setCharAt(0,'X') |
length() |
Returns the current length | sb.length() |
toString() |
Converts to a String object | sb.toString() |
indexOf(str) |
Finds first occurrence of str | sb.indexOf("ab") |
Strings and Multithreading
A String is immutable, so many threads
can safely read the same value without locks. However,
StringBuilder is mutable and should not
be shared between threads unless the application provides
synchronization.
String message = "Payment completed"; Runnable task = () -> System.out.println(message); new Thread(task).start(); new Thread(task).start(); // Safe: both threads only read an immutable String.
StringBuffer synchronizes its public
methods, so concurrent append operations on the same instance are
protected. It is useful when one mutable buffer must be shared by
multiple threads.
StringBuffer auditLog = new StringBuffer();
Runnable logTask = () -> auditLog.append(
Thread.currentThread().getName()).append("\n");
Thread first = new Thread(logTask, "worker-1");
Thread second = new Thread(logTask, "worker-2");
first.start();
second.start();
first.join();
second.join();
System.out.println(auditLog); // append calls are synchronized
StringBuilder is faster because it does
not pay for synchronization. Use it inside one thread, or give each
thread its own builder and combine the results afterward.
// Safe pattern: one builder per thread
Callable<String> work = () -> {
StringBuilder local = new StringBuilder();
local.append("Built by ")
.append(Thread.currentThread().getName());
return local.toString();
};
Choosing a text type
Read-only text ───────────────► String
Mutable, one thread ───────────► StringBuilder
Mutable, shared threads ───────► StringBuffer
(or explicit locking)
Important: synchronized methods protect one StringBuffer operation at a time. A longer sequence such as “check length, then append” may still need an external lock when those actions must be atomic together.
10. String Formatting
String.format() allows you to build
formatted strings using format specifiers, similar to
printf in C. It returns a new String
without printing it, making it highly flexible.
Common Format Specifiers
| Specifier | Type | Example | Output |
|---|---|---|---|
%s |
String |
String.format("%s!", "Hello")
|
Hello! |
%d |
Integer (decimal) | String.format("%d", 42) |
42 |
%f |
Float/double |
String.format("%.2f", 3.14159)
|
3.14 |
%c |
Character | String.format("%c", 'A') |
A |
%b |
Boolean | String.format("%b", true) |
true |
%n |
Newline | (platform-independent) | line break |
%10s |
Right-aligned, width 10 | String.format("%10s","Hi") |
" Hi" |
%-10s |
Left-aligned, width 10 |
String.format("%-10s|","Hi")
|
"Hi |" |
%05d |
Zero-padded int, width 5 | String.format("%05d", 42) |
00042 |
public class StringFormatDemo {
public static void main(String[] args) {
// Basic formatting
String name = "Alice";
int age = 30;
double gpa = 3.856;
String info = String.format("Name: %s | Age: %d | GPA: %.2f", name, age, gpa);
System.out.println(info);
// Building a formatted table
System.out.printf("%-15s %5s %8s%n", "Product", "Qty", "Price");
System.out.printf("%-15s %5d %8.2f%n", "Laptop", 2, 899.99);
System.out.printf("%-15s %5d %8.2f%n", "Mouse", 5, 24.99);
System.out.printf("%-15s %5d %8.2f%n", "Keyboard", 3, 49.99);
// Zero-padded order numbers
for (int i = 1; i <= 5; i++) {
System.out.println("Order #" + String.format("%04d", i));
}
// Formatted currency
double price = 1234.5;
System.out.println(String.format("Total: $%,.2f", price)); // $1,234.50
}
}
Product Qty Price
Laptop 2 899.99
Mouse 5 24.99
Keyboard 3 49.99
Order #0001
Order #0002
Order #0003
Order #0004
Order #0005
Total: $1,234.50
Java 15+ Text Blocks: For multi-line strings,
Java 15 introduced text blocks using triple quotes
"""...""", which preserve formatting
without escape characters — great for JSON, SQL, or HTML templates
embedded in code.
