Back to Strings
Q1Question 1 of 61

What is a String in Java

Key Concepts
  • A String in Java is an immutable object that wraps a sequence of characters and cannot be modified after creation.
  • Every operation like concat, substring, or toUpperCase returns a new String and leaves the original unchanged.
  • String literals are interned in the String pool so identical literals share a single object in memory.
  • Always compare String contents with equals, never with the double-equals operator which checks object identity.
  • Use StringBuilder for efficient string building in loops because repeated concatenation creates many throwaway objects.
  • The String class is declared final so it cannot be subclassed, which protects the immutability guarantee.

Definition

A String is an immutable sequence of characters in Java: you never change it, you only create a new one.
  • String – an object of the java.lang.String class that holds a sequence of characters and cannot be modified after creation.
  • Immutable – once a String is created, its internal character data never changes; every modification method returns a new String.
  • String pool – a special memory area where the JVM stores interned String literals so identical literals share one object.
  • final class – String is declared final, so no subclass can override its behaviour and break the immutability contract.
  • equals vs == – use equals() to compare character content; == compares object references and can give wrong results for Strings created with new.

A String is like a printed cheque at National Bank of India. Once the cheque is printed with Rajesh Kumar and an amount, you cannot erase and rewrite it. If you need a different amount, you print a new cheque. The old cheque still exists exactly as printed. That is why a String never changes: any modification produces a new String, like printing a fresh cheque.

Understanding the Concepts

What is a String in Java?

A String in Java is an object that represents a sequence of characters. Internally, a modern Java String wraps a byte[] array holding the character data plus an encoding flag called coder. The class is declared final so no subclass can exist, and the backing array is private final, which means the data cannot be reassigned or accessed from outside. This combination guarantees immutability: once a String object is created, its contents never change.

In the banking domain at National Bank of India, Rajesh stores an account holder name as a String. That name can be passed to logging, auditing, and display layers without fear that any layer will accidentally alter the original value.

JAVA
package com.bank.core;

public class StringBasics {
    public static void main(String[] args) {
        String accountHolder = "Rajesh Kumar";
        String upper = accountHolder.toUpperCase();

        System.out.println("Original: " + accountHolder);
        System.out.println("Uppercase: " + upper);
        System.out.println("Same object? " + (accountHolder == upper));
    }
}
TEXT
Original: Rajesh Kumar
Uppercase: RAJESH KUMAR
Same object? false

The toUpperCase() method returns a new String. The original accountHolder is untouched.

Why is String immutable?

Immutability gives three important guarantees that matter in production banking systems.

First, thread safety. Because a String cannot change, multiple threads can read the same String without any synchronisation. In a payment gateway at National Bank of India, hundreds of threads read the bank name constant simultaneously with zero risk of corruption.

Second, safe HashMap keys. A HashMap computes the bucket using hashCode(). If the key could change after insertion, the hash would become stale and the entry would be lost. Immutable Strings guarantee the hash never changes, which is why Strings are the most common map key in Java.

Third, security. Sensitive values like database connection strings, account numbers, and class names are stored as Strings. If Strings were mutable, malicious code could change a validated value after the security check.

JAVA
package com.bank.core;

import java.util.HashMap;
import java.util.Map;

public class StringAsKey {
    public static void main(String[] args) {
        Map<String, String> accountMap = new HashMap<>();
        String key = "SAV2001";
        accountMap.put(key, "Rajesh Kumar");

        // key is immutable, so the lookup always works
        System.out.println("Holder: " + accountMap.get("SAV2001"));
    }
}
TEXT
Holder: Rajesh Kumar

How does the String pool work?

The JVM maintains a special memory area called the String pool (also known as the intern pool). When you write a String literal in source code, the compiler interns it: if an identical literal already exists in the pool, the new reference points to the same object. This saves memory when the same string appears many times in code.

JAVA
package com.bank.core;

public class StringPoolDemo {
    public static void main(String[] args) {
        String a = "SAVINGS";
        String b = "SAVINGS";
        String c = new String("SAVINGS");

        System.out.println("a == b : " + (a == b));   // true, same pool object
        System.out.println("a == c : " + (a == c));   // false, c is a new object
        System.out.println("a.equals(c) : " + a.equals(c)); // true, same content
    }
}
TEXT
a == b : true
a == c : false
a.equals(c) : true

Literals a and b share the same pooled object, so == is true. But c was created with new, which always allocates a fresh object on the heap outside the pool. This is why you must always use .equals() for content comparison.

How is String different from StringBuilder?

String is immutable: every modification creates a new object. StringBuilder is mutable: it modifies its internal buffer in place. When you need to build a string by appending many pieces, StringBuilder avoids creating throwaway intermediate String objects.

| Feature | String | StringBuilder |
|---|---|---|
| Mutability | Immutable | Mutable |
| Thread safety | Safe (immutable) | Not thread-safe |
| Performance for concatenation | Slow in loops (new object each time) | Fast (modifies buffer in place) |
| Use case | Store and pass values | Build strings dynamically |

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class StatementBuilder {
    public static void main(String[] args) {
        String[] descriptions = {"Deposit", "EMI Payment", "Transfer to Priya"};
        BigDecimal[] amounts = {
            new BigDecimal("50000.00"),
            new BigDecimal("12500.00"),
            new BigDecimal("8000.00")
        };

        // Wrong: creates many throwaway Strings
        String bad = "";
        for (int i = 0; i < descriptions.length; i++) {
            bad = bad + descriptions[i] + ": Rs." + amounts[i] + "\n";
        }

        // Correct: StringBuilder modifies buffer in place
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < descriptions.length; i++) {
            sb.append(descriptions[i]).append(": Rs.").append(amounts[i]).append("\n");
        }
        String statement = sb.toString();
        System.out.println(statement);
    }
}
TEXT
Deposit: Rs.50000.00
EMI Payment: Rs.12500.00
Transfer to Priya: Rs.8000.00

What are common String methods used in coding problems?

The String class provides many methods that appear frequently in DSA problems. Here are the ones Rajesh uses daily when processing account data at National Bank of India.

  • charAt(int index) returns the character at a given position.
  • length() returns the number of characters.
  • substring(int begin, int end) extracts a portion without modifying the original.
  • toCharArray() converts the String to a char[] for index-based processing.
  • equals(Object other) compares content; equalsIgnoreCase ignores case.
  • indexOf(String str) finds the first occurrence of a substring.
  • trim() removes leading and trailing whitespace.
JAVA
package com.bank.core;

public class StringMethodsDemo {
    public static void main(String[] args) {
        String accountId = "  SAV2001RAJESH  ";

        String trimmed = accountId.trim();
        char first = trimmed.charAt(0);
        String prefix = trimmed.substring(0, 3);
        int nameStart = trimmed.indexOf("RAJESH");

        System.out.println("Trimmed: '" + trimmed + "'");
        System.out.println("First char: " + first);
        System.out.println("Prefix: " + prefix);
        System.out.println("Name starts at index: " + nameStart);
    }
}
TEXT
Trimmed: 'SAV2001RAJESH'
First char: S
Prefix: SAV
Name starts at index: 7

What are common mistakes with Strings?

The most common beginner mistake is ignoring the return value of String methods. Because Strings are immutable, methods like trim(), toUpperCase(), and replace() do not change the original: they return a new String. If you discard the return value, nothing happens.

JAVA
package com.bank.core;

public class StringMistakes {
    public static void main(String[] args) {
        String input = "  SAV2001  ";

        // WRONG: return value discarded
        input.trim();
        System.out.println("Wrong: '" + input + "'");

        // CORRECT: assign the return value
        input = input.trim();
        System.out.println("Correct: '" + input + "'");
    }
}
TEXT
Wrong: '  SAV2001  '
Correct: 'SAV2001'

Another common mistake is comparing Strings with == instead of .equals(). The == operator checks whether two references point to the same object, not whether the content is the same. This works for interned literals but fails for Strings created at runtime.

JAVA
package com.bank.core;

public class EqualsVsDoubleEquals {
    public static void main(String[] args) {
        String fromDb = new String("SAV2001");
        String fromCode = "SAV2001";

        System.out.println("== : " + (fromDb == fromCode));       // false
        System.out.println(".equals: " + fromDb.equals(fromCode)); // true
    }
}
TEXT
== : false
.equals: true

What about String performance and memory?

Before Java 9, String stored characters in a char[] where each character used 2 bytes (UTF-16). Since Java 9, the Compact Strings feature stores Latin-1 characters in a byte[] using 1 byte each and uses 2 bytes only for characters that need it. This roughly halves memory for ASCII-heavy banking data like account IDs and transaction codes.

String concatenation with + in a loop is the classic performance trap. Each + creates a new String and copies all previous characters, making the loop O(n squared) in total. StringBuilder avoids this by maintaining a resizable buffer.

For very large text processing, such as generating bank statements with thousands of lines, Pankaj the architect recommends pre-sizing the StringBuilder with an estimated capacity to avoid repeated buffer resizing.

JAVA
package com.bank.service;

public class PreSizedBuilder {
    public static void main(String[] args) {
        int estimatedLines = 1000;
        int avgLineLength = 80;
        StringBuilder sb = new StringBuilder(estimatedLines * avgLineLength);

        for (int i = 0; i < estimatedLines; i++) {
            sb.append("TXN").append(i).append(": Debit Rs.500.00\n");
        }
        System.out.println("Statement length: " + sb.length() + " chars");
    }
}
TEXT
Statement length: 27890 chars

What about Strings and coding problems?

In DSA interview problems, Strings are one of the most frequently tested topics. The key techniques include:

  • Two pointers for palindrome checks, comparing characters from both ends moving inward.
  • Frequency counting with an int[26] array or a HashMap for anagram checks and character tallies.
  • Sliding window for finding substrings with specific properties like longest substring without repeating characters.
  • StringBuilder for reversing strings or building results character by character.

At National Bank of India, Rajesh converts account data to char[] for index-based processing because charAt() on each call is slightly slower than direct array access in tight loops.

JAVA
package com.bank.core;

public class StringForDSA {
    public static void main(String[] args) {
        String accountCode = "SAVINGS";

        // Convert to char array for fast index access
        char[] chars = accountCode.toCharArray();

        // Count character frequencies
        int[] freq = new int[26];
        for (char c : chars) {
            freq[c - 'A']++;
        }

        // Print frequencies of non-zero characters
        for (int i = 0; i < 26; i++) {
            if (freq[i] > 0) {
                System.out.println((char) ('A' + i) + ": " + freq[i]);
            }
        }
    }
}
TEXT
A: 1
G: 1
I: 1
N: 1
S: 2
V: 1

What are edge cases to watch for?

When solving String problems, always consider these edge cases:

  • Null input can cause a NullPointerException. Guard with a null check before calling any String method.
  • Empty string has length zero. Methods like charAt(0) will throw StringIndexOutOfBoundsException.
  • Single character string is both a palindrome and its own reverse.
  • Unicode characters may need special handling. length() returns the number of char units, not code points, so a supplementary character counts as two.
JAVA
package com.bank.core;

public class StringEdgeCases {
    public static void main(String[] args) {
        String empty = "";
        String single = "A";
        String nullStr = null;

        System.out.println("Empty length: " + empty.length());
        System.out.println("Single reversed: " + new StringBuilder(single).reverse());

        // Safe null check pattern
        if (nullStr != null && nullStr.length() > 0) {
            System.out.println(nullStr.charAt(0));
        } else {
            System.out.println("Input is null or empty");
        }
    }
}
TEXT
Empty length: 0
Single reversed: A
Input is null or empty

Visual Overview

DIAGRAM
  String Lifecycle in Java
  ========================

  Source Code           Memory Layout
  ───────────           ─────────────
  String a = "SAV";     ┌──────────────────────┐
                        │     String Pool        │
  String b = "SAV";     │  ┌─────────────────┐  │
       │                │  │  "SAV" (one obj) │  │
       └── a ──────────►│  └─────────────────┘  │
       └── b ──────────►│        ▲               │
                        └────────│───────────────┘
  String c = new        ┌────────│───────────────┐
    String("SAV");      │  Heap  │               │
       │                │  ┌─────────────────┐  │
       └── c ──────────►│  │  "SAV" (copy)   │  │
                        │  └─────────────────┘  │
                        └────────────────────────┘

  a == b   → true  (same pool object)
  a == c   → false (different objects)
  a.equals(c) → true  (same content)
DIAGRAM
  String vs StringBuilder
  ═══════════════════════

  String concatenation in a loop:
  ┌───┐   +"A"   ┌────┐   +"B"   ┌─────┐
  │ "" │ ──────► │"A" │ ──────► │"AB" │ ... (new object each time)
  └───┘          └────┘          └─────┘

  StringBuilder in a loop:
  ┌───────────────────────┐
  │  Internal buffer []   │
  │  append("A")          │  (same object, buffer grows)
  │  append("B")          │
  │  toString() → "AB"    │
  └───────────────────────┘

Key Takeaway

A String in Java is an object representing a sequence of characters, and its single most important property is immutability. Once a String is created, its contents can never change. Every method that appears to modify a String, such as substring, concat, or toUpperCase, actually returns a brand new String and leaves the original completely untouched. The String class is declared final, so it cannot be subclassed, and its backing data is private final, which locks down the immutability guarantee.

In the banking project at National Bank of India, this property is what makes PII masking safe. When Rajesh masks an account ID to show only the first two characters, the masking produces a new String, and the real account ID stored in memory and the database is never altered, which is critical for auditing. The same property allows Strings to be shared freely across threads without synchronisation and used as HashMap keys, because an immutable object has a hash code that never changes after creation.

The practical consequence every developer must remember is comparison. Because a String literal written in code is interned in the String pool while a String created with the new keyword is a separate object, two Strings can hold the same characters but be different objects in memory. Always compare String contents with the equals method, never with the double-equals operator, because double-equals compares object identity and would silently fail when a value read from a database or a web form does not happen to be the same pooled object as a literal in your code.

For performance, use StringBuilder when building a String in a loop, because repeated concatenation of immutable Strings creates many throwaway objects and makes the loop O(n squared). Pre-size the StringBuilder when the approximate output size is known to avoid repeated buffer resizing.

Remember one sentence: a String in Java is immutable, so every modification creates a new object, and you must use equals for content comparison and StringBuilder for efficient building.

---

Share
Use ← → arrow keys to navigate