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 new keyword (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
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)
false
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
Java
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:

  1. Declare the class final.
  2. Make all fields private final.
  3. Set fields only through the constructor.
  4. Do not provide setters or methods that mutate state.
  5. 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
    }
}
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)
Hello, World!
Age: 25

The concat() Method

String s1 = "Good ";
String s2 = s1.concat("Morning");
System.out.println(s2);   // Good Morning
// s1 is unchanged: "Good "
Good Morning

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
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 + "] ");
        }
    }
}
Hello, Java World!
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, World
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.
    }
}
Learning Java is fun!
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
    }
}
Name: Alice | Age: 30 | GPA: 3.86
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.

Continue learning