Chapter 11 Intermediate 32 min min read Updated 2026-04-08

Strings in Java

Practice Questions →

In This Chapter

What Is It?

What Are Strings in Java?

A String in Java is an object that represents a sequence of characters. Unlike primitive data types such as int or char, strings in Java are objects of the java.lang.String class. This distinction is critical because it means strings have methods, can be passed as objects, and follow object-oriented rules including immutability.

Every Java program that prints output, reads user input, or processes text relies on strings. From storing a student's name to building SQL queries, from parsing JSON responses to formatting log messages, strings are the backbone of text processing in Java.

Creating Strings

Java provides two primary ways to create strings:

1. String Literal: String name = "Aarav"; — Created using double quotes. Java stores this in a special memory area called the String Pool. If another variable is assigned the same literal, Java reuses the existing object from the pool instead of creating a new one. This is memory-efficient.

2. Using the new keyword: String name = new String("Aarav"); — Explicitly creates a new String object on the heap, bypassing the String Pool. Even if an identical string exists in the pool, a new object is created. This is rarely needed in practice.

Unlike Python and JavaScript, Java does not support single quotes for strings. Single quotes in Java are reserved for the char data type: char ch = 'A';.

Why Does It Matter?

Why Are Strings Important in Java?

Strings are one of the most heavily tested topics in Java interviews and university exams. Here is why mastering them is non-negotiable for any serious Java developer:

1. Strings Are Everywhere in Java Applications

Every System.out.println() call involves a string. Every Scanner.nextLine() returns a string. Configuration files, database queries, API endpoints, file paths, error messages — all are strings. You cannot write a meaningful Java program without manipulating strings.

2. Immutability Is a Core Interview Concept

Java strings are immutable — once created, their content cannot be changed. This single property leads to dozens of interview questions about the String Pool, memory management, thread safety, and why StringBuilder exists. Understanding immutability is understanding Java's design philosophy.

3. The == vs .equals() Trap

One of the most common bugs in Java programs comes from using == instead of .equals() to compare strings. This distinction between reference equality and content equality is fundamental to Java and is tested in virtually every placement exam.

4. Performance-Critical String Operations

Concatenating strings in a loop using + creates a new object every iteration, leading to O(n^2) performance. Knowing when to use StringBuilder versus the + operator separates junior developers from those who understand Java internals.

5. Foundation for Collections, I/O, and Regex

Strings interact with almost every part of the Java ecosystem: they are keys in HashMaps, they are read from and written to files, they are matched against regular expressions, and they are serialized to JSON. Weak string skills create a bottleneck across all these areas.

Detailed Explanation

Detailed Explanation

1. String Immutability

In Java, strings are immutable. Once a String object is created, its character sequence cannot be modified. Every operation that appears to modify a string actually creates a new String object.

String s = "Hello";
s.toUpperCase(); // Returns "HELLO" but s is still "Hello"
s = s.toUpperCase(); // Now s points to the new "HELLO" object

Why is this important? Immutability provides three key benefits: (1) Thread safety — multiple threads can share the same String without synchronization. (2) Security — strings used as class names, file paths, or network addresses cannot be altered after creation. (3) Hashcode caching — since the content never changes, the hashcode can be computed once and reused, making strings efficient as HashMap keys.

2. The String Pool

Java maintains a special memory area called the String Pool (or String Intern Pool) within the heap. When you create a string using a literal, Java first checks if an identical string already exists in the pool. If it does, the new variable simply references the existing object. If not, a new object is created in the pool.

String a = "Hello";
String b = "Hello";
System.out.println(a == b); // true — same object in the pool

When you use new String("Hello"), Java creates a new object on the heap regardless of whether "Hello" exists in the pool. The pool object is still created (for the literal), but the variable points to the heap object.

String c = new String("Hello");
System.out.println(a == c); // false — different objects
System.out.println(a.equals(c)); // true — same content

The intern() method forces a string into the pool: String d = c.intern(); After this, a == d would be true.

3. String Comparison: == vs .equals() vs .compareTo()

This is one of the most critical distinctions in Java:

== (Reference Equality): Checks whether two variables point to the exact same object in memory. It does NOT compare the content of the strings.

.equals() (Content Equality): Checks whether two strings contain the same sequence of characters. This is what you should use for string comparison in almost all cases.

.equalsIgnoreCase(): Same as .equals() but ignores case differences. "Hello".equalsIgnoreCase("hello") returns true.

.compareTo() (Lexicographic Comparison): Returns 0 if strings are equal, a negative value if the calling string comes before the argument, and a positive value if it comes after. Used for sorting.

"apple".compareTo("banana"); // negative (a comes before b)
"banana".compareTo("apple"); // positive (b comes after a)
"apple".compareTo("apple");  // 0 (equal)

4. Essential String Methods

The String class provides over 60 methods. Here are the ones you must know:

Length and Character Access

length() returns the number of characters. charAt(int index) returns the character at the given index (0-based). toCharArray() converts the string to a character array.

Searching

indexOf(String str) returns the index of the first occurrence, or -1 if not found. lastIndexOf(String str) returns the index of the last occurrence. contains(CharSequence s) returns true if the string contains the given sequence.

Extraction

substring(int beginIndex) returns a substring from the given index to the end. substring(int beginIndex, int endIndex) returns a substring from beginIndex (inclusive) to endIndex (exclusive).

Case Conversion

toUpperCase() and toLowerCase() return new strings with all characters converted to the respective case.

Trimming and Replacing

trim() removes leading and trailing whitespace. replace(char oldChar, char newChar) replaces all occurrences of a character. replace(CharSequence target, CharSequence replacement) replaces all occurrences of a substring.

Splitting and Joining

split(String regex) splits the string into an array based on a delimiter pattern. String.join(CharSequence delimiter, CharSequence... elements) joins multiple strings with a delimiter (Java 8+).

Checking Content

startsWith(String prefix) and endsWith(String suffix) check the beginning and end of a string. isEmpty() returns true if the string has length 0. isBlank() returns true if the string is empty or contains only whitespace (Java 11+).

Conversion

String.valueOf(int i) converts a primitive to a string. toCharArray() converts to a char array. getBytes() converts to a byte array.

5. StringBuilder — Mutable Strings

Since strings are immutable, repeated concatenation in a loop creates many temporary objects, leading to poor performance. StringBuilder solves this by providing a mutable character sequence.

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
    sb.append("Item " + i + ", ");
}
String result = sb.toString();

Key StringBuilder methods: append() adds text at the end. insert(int offset, String str) inserts text at a position. delete(int start, int end) removes characters. reverse() reverses the sequence. toString() converts back to a String.

6. StringBuffer — Thread-Safe Mutable Strings

StringBuffer is identical to StringBuilder in functionality but all its methods are synchronized, making it thread-safe. The trade-off is slower performance due to synchronization overhead. Use StringBuilder in single-threaded contexts (which is most of the time) and StringBuffer only when multiple threads access the same mutable string.

7. String Formatting

Java provides two main ways to format strings:

String.format(): Returns a formatted string without printing it.

String msg = String.format("Name: %s, Age: %d, GPA: %.2f", "Priya", 20, 9.45);

System.out.printf(): Prints a formatted string directly to the console.

System.out.printf("Score: %d out of %d%n", 85, 100);

Common format specifiers: %s for strings, %d for integers, %f for floating-point numbers, %.2f for 2 decimal places, %n for a platform-independent newline.

8. String Concatenation Performance

The + operator for string concatenation is convenient but has a hidden cost. In a loop, each + creates a new String object, copies the old content plus the new content, and discards the old object. For n iterations, this results in O(n^2) time complexity.

// BAD: O(n^2) — creates n intermediate String objects
String result = "";
for (int i = 0; i < 10000; i++) {
    result += i + ","; // new object every iteration
}

// GOOD: O(n) — single StringBuilder, no copying
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(",");
}
String result = sb.toString();

Note: The Java compiler optimizes simple concatenation like "Hello" + name using StringBuilder internally. The problem arises only in loops.

Code Examples

String Creation — Literal vs new
public class StringCreation {
    public static void main(String[] args) {
        // String literal — stored in String Pool
        String s1 = "Hello";
        String s2 = "Hello";

        // Using new — creates a new object on the heap
        String s3 = new String("Hello");

        System.out.println(s1 == s2);       // true (same pool object)
        System.out.println(s1 == s3);       // false (different objects)
        System.out.println(s1.equals(s3));  // true (same content)

        // intern() — moves heap string to the pool
        String s4 = s3.intern();
        System.out.println(s1 == s4);       // true (same pool object)
    }
}
String literals are stored in the String Pool, so s1 and s2 point to the same object (== returns true). new String() always creates a separate heap object, so s1 == s3 is false even though the content is identical. .equals() compares content and returns true. intern() returns the pool reference for a given string, so s1 == s4 is true.
true false true true
String Comparison — == vs .equals() vs .compareTo()
public class StringComparison {
    public static void main(String[] args) {
        String a = "Java";
        String b = "Java";
        String c = new String("Java");
        String d = "java";

        // == checks reference (same object?)
        System.out.println(a == b);              // true
        System.out.println(a == c);              // false

        // .equals() checks content (same characters?)
        System.out.println(a.equals(c));         // true
        System.out.println(a.equals(d));         // false (case matters)

        // .equalsIgnoreCase() ignores case
        System.out.println(a.equalsIgnoreCase(d)); // true

        // .compareTo() returns int for lexicographic order
        System.out.println("apple".compareTo("banana")); // negative
        System.out.println("banana".compareTo("apple")); // positive
        System.out.println("apple".compareTo("apple"));  // 0
    }
}
== checks if two references point to the same object in memory. For literals in the pool, this is true. For objects created with new, it is false. .equals() compares the actual character content and is the correct way to compare strings in almost all situations. .compareTo() returns 0 for equal strings, a negative number if the caller is lexicographically smaller, and a positive number if it is larger.
true false true false true -1 1 0
Essential String Methods
public class StringMethods {
    public static void main(String[] args) {
        String s = "  Modern Age Coders  ";

        // Length and trimming
        System.out.println(s.length());          // 21
        System.out.println(s.trim());            // "Modern Age Coders"
        System.out.println(s.trim().length());   // 17

        String name = "Kavitha Suresh";

        // Character access
        System.out.println(name.charAt(0));      // K
        System.out.println(name.charAt(name.length() - 1)); // h

        // Case conversion
        System.out.println(name.toUpperCase());  // KAVITHA SURESH
        System.out.println(name.toLowerCase());  // kavitha suresh

        // Search methods
        System.out.println(name.indexOf("a"));       // 1
        System.out.println(name.lastIndexOf("a"));   // 9
        System.out.println(name.contains("Suresh")); // true
        System.out.println(name.indexOf("xyz"));     // -1

        // Substring
        System.out.println(name.substring(0, 7));    // Kavitha
        System.out.println(name.substring(8));       // Suresh
    }
}
length() returns character count including spaces. trim() removes leading and trailing whitespace. charAt() returns the character at a given zero-based index. indexOf() returns the first occurrence of a substring or -1 if not found. lastIndexOf() returns the last occurrence. contains() returns a boolean. substring(start, end) extracts characters from start (inclusive) to end (exclusive).
21 Modern Age Coders 17 K h KAVITHA SURESH kavitha suresh 1 9 true -1 Kavitha Suresh
split(), join(), replace(), and startsWith()/endsWith()
public class StringOperations {
    public static void main(String[] args) {
        // split()
        String csv = "Aarav,Priya,Rohan,Meera";
        String[] names = csv.split(",");
        for (String name : names) {
            System.out.println(name);
        }

        // String.join() — Java 8+
        String joined = String.join(" | ", names);
        System.out.println(joined);

        // replace()
        String text = "Java is hard. Java is confusing.";
        System.out.println(text.replace("hard", "fun"));
        System.out.println(text.replace('a', 'o'));

        // startsWith() and endsWith()
        String file = "Report2026.pdf";
        System.out.println(file.startsWith("Report")); // true
        System.out.println(file.endsWith(".pdf"));     // true
        System.out.println(file.endsWith(".doc"));     // false
    }
}
split() takes a regex pattern and returns a String array. String.join() is a static method that concatenates elements with a delimiter. replace() has two overloads: one for single characters and one for CharSequences. startsWith() and endsWith() check prefixes and suffixes and are commonly used for file type validation.
Aarav Priya Rohan Meera Aarav | Priya | Rohan | Meera Java is fun. Java is confusing. Jovo is hord. Jovo is confusing. true true false
StringBuilder — Mutable String Operations
public class StringBuilderDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");

        // append()
        sb.append(" World");
        System.out.println(sb);              // Hello World

        // insert()
        sb.insert(5, ",");
        System.out.println(sb);              // Hello, World

        // delete()
        sb.delete(5, 7);                     // Remove ", "
        System.out.println(sb);              // HelloWorld

        // reverse()
        sb.reverse();
        System.out.println(sb);              // dlroWolleH

        // replace()
        sb.reverse();                        // back to HelloWorld
        sb.replace(0, 5, "Namaste");
        System.out.println(sb);              // NamasteWorld

        // length and charAt
        System.out.println(sb.length());     // 12
        System.out.println(sb.charAt(0));    // N

        // Convert to String
        String result = sb.toString();
        System.out.println(result);          // NamasteWorld
    }
}
StringBuilder is mutable, so all methods modify the same object instead of creating new ones. append() adds to the end. insert() adds at a specific position. delete(start, end) removes characters from start (inclusive) to end (exclusive). reverse() reverses in place. replace(start, end, str) replaces a range with a new string. Always call toString() when you need an immutable String.
Hello World Hello, World HelloWorld dlroWolleH NamasteWorld 12 N NamasteWorld
String Formatting with String.format() and printf()
public class StringFormatting {
    public static void main(String[] args) {
        String name = "Deepak";
        int age = 21;
        double gpa = 8.756;

        // String.format()
        String info = String.format("Name: %s, Age: %d, GPA: %.2f", name, age, gpa);
        System.out.println(info);

        // printf() — prints directly
        System.out.printf("%-15s %5d %8.2f%n", "Deepak", 21, 8.756);
        System.out.printf("%-15s %5d %8.2f%n", "Sneha", 20, 9.120);
        System.out.printf("%-15s %5d %8.2f%n", "Vikram", 22, 7.890);

        // Padding with zeros
        System.out.printf("Roll No: %05d%n", 42);

        // valueOf()
        int num = 100;
        String str = String.valueOf(num);
        System.out.println(str + " is a String: " + (str instanceof String));
    }
}
String.format() works like printf but returns a String instead of printing. %s is for strings, %d for integers, %.2f for two decimal places. %-15s left-aligns a string in a 15-character field. %05d zero-pads an integer to 5 digits. %n is the platform-independent newline. String.valueOf() converts any primitive to its String representation.
Name: Deepak, Age: 21, GPA: 8.76 Deepak 21 8.76 Sneha 20 9.12 Vikram 22 7.89 Roll No: 00042 100 is a String: true
String Concatenation Performance — Why StringBuilder Matters
public class ConcatPerformance {
    public static void main(String[] args) {
        int n = 50000;

        // BAD: String concatenation in a loop — O(n^2)
        long start1 = System.currentTimeMillis();
        String bad = "";
        for (int i = 0; i < n; i++) {
            bad += i;
        }
        long time1 = System.currentTimeMillis() - start1;
        System.out.println("String + loop: " + time1 + " ms");

        // GOOD: StringBuilder — O(n)
        long start2 = System.currentTimeMillis();
        StringBuilder good = new StringBuilder();
        for (int i = 0; i < n; i++) {
            good.append(i);
        }
        String result = good.toString();
        long time2 = System.currentTimeMillis() - start2;
        System.out.println("StringBuilder: " + time2 + " ms");

        System.out.println("Both produce same result: " + bad.equals(result));
    }
}
String concatenation with + in a loop creates a new String object on every iteration, copying all previous characters plus the new ones. For n iterations, this results in approximately n*(n+1)/2 character copies, which is O(n^2). StringBuilder maintains a resizable internal buffer and only copies characters when the buffer needs to expand, resulting in O(n) performance. For 50,000 iterations, the difference can be hundreds of milliseconds versus single-digit milliseconds.
String + loop: 890 ms StringBuilder: 3 ms Both produce same result: true
toCharArray(), valueOf(), and Character Checking
public class StringCharOps {
    public static void main(String[] args) {
        String s = "Hello World 123";

        // toCharArray()
        char[] chars = s.toCharArray();
        System.out.println("Length: " + chars.length);
        System.out.println("First char: " + chars[0]);

        // Count digits, letters, and spaces
        int digits = 0, letters = 0, spaces = 0;
        for (char c : chars) {
            if (Character.isDigit(c)) digits++;
            else if (Character.isLetter(c)) letters++;
            else if (c == ' ') spaces++;
        }
        System.out.println("Digits: " + digits);
        System.out.println("Letters: " + letters);
        System.out.println("Spaces: " + spaces);

        // valueOf() conversions
        System.out.println(String.valueOf(42));      // "42"
        System.out.println(String.valueOf(3.14));    // "3.14"
        System.out.println(String.valueOf(true));    // "true"
        System.out.println(String.valueOf('A'));     // "A"
    }
}
toCharArray() converts a String into a char[], useful for character-by-character processing. The Character wrapper class provides static methods like isDigit(), isLetter(), isUpperCase(), and isLowerCase() for checking individual characters. String.valueOf() is overloaded to accept any primitive type and returns its string representation.
Length: 15 First char: H Digits: 3 Letters: 10 Spaces: 2 42 3.14 true A

Common Mistakes

Using == Instead of .equals() to Compare Strings

String a = new String("Hello");
String b = new String("Hello");
if (a == b) {
    System.out.println("Equal");
} else {
    System.out.println("Not equal"); // This runs!
}
No compilation error, but the comparison checks references, not content. Both objects have the same content but are different objects on the heap.
String a = new String("Hello");
String b = new String("Hello");
if (a.equals(b)) {
    System.out.println("Equal"); // This runs correctly
}
== compares object references (memory addresses), not content. Two String objects created with new are always different objects even if they contain the same characters. Always use .equals() for string content comparison. This is one of the most common bugs in Java and is asked in virtually every interview.

Calling a Method on a Null String

String name = null;
System.out.println(name.length()); // NullPointerException
Exception in thread "main" java.lang.NullPointerException
String name = null;
if (name != null) {
    System.out.println(name.length());
} else {
    System.out.println("Name is not set");
}
// Or use a constant on the left: "Hello".equals(name) — safe even if name is null
Calling any method on a null reference throws a NullPointerException. Always check for null before calling methods. A common defensive technique is to put the known non-null string on the left: "expected".equals(name) instead of name.equals("expected").

Forgetting That String Methods Return New Strings

String name = "  aarav  ";
name.trim();
name.toUpperCase();
System.out.println(name); // Still "  aarav  "
No error, but the original string is unchanged because the return values were not saved.
String name = "  aarav  ";
name = name.trim().toUpperCase();
System.out.println(name); // AARAV
Since strings are immutable, methods like trim() and toUpperCase() do not modify the original string. They return a new String object. You must assign the result back to a variable. Method chaining works: name.trim().toUpperCase() first trims, then converts the trimmed result to uppercase.

String Concatenation with + in a Loop

String result = "";
for (int i = 0; i < 10000; i++) {
    result += i + ",";
}
// Extremely slow — O(n^2) time, creates 10000 temporary objects
No compilation error, but severe performance degradation. Each += creates a new String object and copies all previous characters.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
    sb.append(i).append(",");
}
String result = sb.toString();
// Fast — O(n) time, single mutable object
The + operator in a loop causes Java to create a new String object on every iteration, copying the entire accumulated string plus the new part. For n iterations, the total character copies are approximately n^2/2. Use StringBuilder in loops to maintain a single mutable buffer that grows efficiently.

Using length Instead of length() for Strings

String name = "Sneha";
System.out.println(name.length); // Compilation error
error: cannot find symbol — symbol: variable length
String name = "Sneha";
System.out.println(name.length()); // 5

// Note: arrays use .length (no parentheses)
int[] arr = {1, 2, 3};
System.out.println(arr.length); // 3
In Java, strings use length() (a method), while arrays use length (a field). This inconsistency is a frequent source of errors. Strings are objects, so they have a method. Arrays are special objects with a public length field. Collections use size(). Three different ways to get "length" in Java.

IndexOutOfBoundsException with charAt() or substring()

String s = "Hello";
System.out.println(s.charAt(5)); // StringIndexOutOfBoundsException
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 5
String s = "Hello";
// Valid indices are 0 to s.length()-1
System.out.println(s.charAt(s.length() - 1)); // 'o'

// Always check bounds before accessing
int index = 5;
if (index >= 0 && index < s.length()) {
    System.out.println(s.charAt(index));
} else {
    System.out.println("Index out of range");
}
String indices in Java are 0-based. For a string of length 5, valid indices are 0 through 4. Accessing index 5 (or any index >= length or < 0) throws a StringIndexOutOfBoundsException. Unlike Python, Java does not support negative indexing. Always validate indices before using charAt() or substring().

Summary

  • Strings in Java are immutable objects of the java.lang.String class. Once created, their content cannot be changed. Every modification creates a new String object.
  • String literals are stored in the String Pool for memory efficiency. The new keyword bypasses the pool and always creates a separate heap object.
  • Use .equals() to compare string content, not ==. The == operator checks reference equality (same object), while .equals() checks content equality (same characters). This is the single most important rule for Java strings.
  • Key String methods: length(), charAt(), substring(), indexOf(), lastIndexOf(), contains(), startsWith(), endsWith(), toUpperCase(), toLowerCase(), trim(), replace(), split(), and String.join().
  • StringBuilder provides a mutable character sequence for efficient string building. Use it in loops instead of the + operator to avoid O(n^2) performance degradation.
  • StringBuffer is the thread-safe version of StringBuilder. Use StringBuilder in single-threaded code (most cases) and StringBuffer only when thread safety is required.
  • String.format() and System.out.printf() provide C-style formatting with %s (string), %d (integer), %f (float), and %.Nf (N decimal places).
  • compareTo() enables lexicographic comparison: returns 0 for equal strings, negative if the caller is smaller, positive if larger. Used for sorting.
  • toCharArray() converts a String to a char array for character-by-character processing. The Character class provides isDigit(), isLetter(), isUpperCase(), and other checking methods.
  • The intern() method returns the String Pool reference for any string, enabling == comparison. Useful for performance optimization when comparing the same set of strings repeatedly.

Ready to Practice?

Test your understanding with 50+ practice questions on this topic.

Go to Practice Questions

Want to learn Java with a live mentor?

Explore our Java Masterclass