Why Are Strings Immutable in Java?

A String in Java is an object whose characters are stored in an internal array — a char[] up to Java 8, and a more compact byte[] since Java 9. That array is private and never changed after the String is created.

Since the contents of a String cannot be modified after it is created, Strings are immutable. Whenever you perform an operation that appears to modify a String, Java creates a new String object instead of changing the existing one.

Example

String str = "Hello";
str = str + " World";

What Happens?

  1. "Hello" is created.
  2. "Hello World" is created as a new String object.
  3. The reference str now points to the new object.

The original "Hello" object remains unchanged.

Benefits of String Immutability

  • Improves security.
  • Enables String Pool optimization.
  • Makes Strings thread-safe.
  • Allows efficient caching of hash codes.
  • Prevents accidental modification of shared objects.

Advertisement

StringBuffer vs StringBuilder

StringBuffer and StringBuilder are mutable string classes, meaning their contents can be modified without creating a new object.

Unlike the String class, both allow efficient string manipulation.

StringBuffer

  • Mutable.
  • Thread-safe (methods are synchronized).
  • Slightly slower because of synchronization.
  • Suitable for multithreaded applications.

StringBuilder

  • Mutable.
  • Not thread-safe.
  • Faster than StringBuffer.
  • Suitable for single-threaded applications.

String vs StringBuffer vs StringBuilder

Feature String StringBuffer StringBuilder
Mutable ❌ No ✅ Yes ✅ Yes
Thread-safe Yes (Immutable) ✅ Yes ❌ No
Performance Slow for frequent modifications Slower than StringBuilder Fastest
Best Use Read-only text Multithreaded applications Single-threaded applications

Interview Tip: Use StringBuilder unless thread safety is required. Use StringBuffer only in multithreaded environments.


Do Wrapper Classes Override hashCode() and equals()?

Yes.

Wrapper classes such as:

  • Integer
  • Character
  • Boolean
  • Double
  • Float
  • Long

override both the equals() and hashCode() methods.

This is because wrapper classes are immutable value objects.


equals() Method

The equals() method performs value-based comparison.

Two wrapper objects are considered equal if they store the same value, even if they are different objects in memory.

Example

Integer a = Integer.valueOf(5);
Integer b = Integer.valueOf(5);

System.out.println(a.equals(b));

Note: older examples use new Integer(5). That constructor is deprecated and marked for removal — javac now warns "Integer(int) in Integer has been deprecated and marked for removal". Use Integer.valueOf() or autoboxing (Integer a = 5;).

Output

true

Although a and b are different objects, they contain the same value.


hashCode() Method

The hashCode() method is overridden so that equal objects produce the same hash code.

This is required for proper functioning of hash-based collections such as:

  • HashMap
  • HashSet
  • Hashtable

Objects with equal values generate identical hash codes, allowing efficient searching and retrieval.


Real-Life Example

Imagine two ₹10 currency notes.

Although they are different physical notes, they represent the same monetary value.

Similarly, two Integer objects containing 10 are considered equal.


Real-Time Testing Example

In automation frameworks, wrapper classes are commonly used to store:

  • Retry counts
  • Timeout values
  • Test priorities
  • Status codes

Collections such as HashSet and HashMap rely on equals() and hashCode() to correctly compare these values.


String Encoding and Character Sets in Java

A Java String is a sequence of characters, but files, HTTP requests and databases store bytes. A character set (charset) such as UTF-8 is the rule that turns characters into bytes and back. If one side writes with one charset and the other side reads with another, you get garbled text like é instead of é — a very common bug in API and database tests.

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class StringEncoding {
    public static void main(String[] args) {
        String text = "Price: \u20B9499 \u2014 caf\u00E9";              // rupee sign, em dash, e-acute

        System.out.println("Default charset: " + Charset.defaultCharset());
        System.out.println("Characters: " + text.length());

        byte[] utf8 = text.getBytes(StandardCharsets.UTF_8);          // String -> bytes
        byte[] latin1 = text.getBytes(StandardCharsets.ISO_8859_1);
        System.out.println("UTF-8 bytes: " + utf8.length + ", ISO-8859-1 bytes: " + latin1.length);
        System.out.println("UTF-8 bytes of the rupee sign: " + Arrays.toString("\u20B9".getBytes(StandardCharsets.UTF_8)));

        String back = new String(utf8, StandardCharsets.UTF_8);         // bytes -> String, same charset
        System.out.println("Decoded with UTF-8 matches: " + back.equals(text));

        String wrong = new String(utf8, StandardCharsets.ISO_8859_1);   // wrong charset = garbled text
        System.out.println("Decoded with ISO-8859-1: " + wrong);

        String lost = new String(latin1, StandardCharsets.ISO_8859_1);  // characters outside Latin-1 are lost
        System.out.println("Round trip through ISO-8859-1: " + lost);
    }
}

Output

Default charset: UTF-8
Characters: 18
UTF-8 bytes: 23, ISO-8859-1 bytes: 18
UTF-8 bytes of the rupee sign: [-30, -126, -71]
Decoded with UTF-8 matches: true
Decoded with ISO-8859-1: Price: ₹499 — café
Round trip through ISO-8859-1: Price: ?499 ? café
  • getBytes(StandardCharsets.UTF_8) turns a String into bytes; new String(bytes, StandardCharsets.UTF_8) turns them back.
  • The rupee sign takes 3 bytes in UTF-8, so 18 characters became 23 bytes.
  • Reading UTF-8 bytes as ISO-8859-1 produced â¹ and é — the classic "mojibake" pattern. When you see à or â in a test failure, suspect a charset mismatch.
  • ISO-8859-1 has no rupee sign or em dash, so those characters were replaced by ? and lost for good.

Encoding Rules for Testers

  • Always pass the charset explicitly — StandardCharsets.UTF_8 — instead of relying on the default.
  • Java 18 and later use UTF-8 as the default charset everywhere. On Java 17 and earlier the default depends on the operating system (often Windows-1252 on Windows), so the same test can pass on a Mac and fail on a Windows CI agent.
  • Compile sources as UTF-8: on a machine with a non-UTF-8 locale, javac rejects non-English characters in a file with StringEncoding.java:7: error: unmappable character (0xE2) for encoding US-ASCII. Use javac -encoding UTF-8, or in Maven set <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>.
  • Check the API and database: responses should declare Content-Type: application/json; charset=utf-8, and MySQL tables should use utf8mb4 (plain utf8 in MySQL can't store emoji).

Exception Handling in Java

Exception handling is a mechanism for handling runtime errors without terminating the program unexpectedly.

It allows applications to recover from errors and continue normal execution whenever possible.


Building Blocks of Exception Handling

Java provides five main keywords for exception handling:

  • try
  • catch
  • finally
  • throw
  • throws

try Block

The try block contains code that may throw an exception.


catch Block

The catch block handles exceptions thrown inside the corresponding try block.

You can have multiple catch blocks to handle different exception types.


finally Block

The finally block always executes, regardless of whether an exception occurs.

It is commonly used for cleanup tasks such as:

  • Closing database connections.
  • Closing files.
  • Releasing system resources.

throw Keyword

The throw keyword is used to explicitly throw an exception.

Example:

throw new ArithmeticException("Invalid operation");

throws Keyword

The throws keyword declares that a method may throw one or more exceptions.

It shifts the responsibility of handling the exception to the calling method.

Example:

public void readFile() throws IOException {
    // code
}

Exception Handling Example

public class ExceptionExample {

    public static void main(String[] args) {

        try {

            int divideByZero = 5 / 0;

        } catch (ArithmeticException e) {

            System.out.println(
                "ArithmeticException = " + e.getMessage()
            );

        } finally {

            System.out.println(
                "This is the finally block"
            );
        }
    }
}

Output

ArithmeticException = / by zero
This is the finally block

Why Exception Handling Is Important

Exception handling:

  • Prevents unexpected application crashes.
  • Separates error-handling code from business logic.
  • Improves application reliability.
  • Makes debugging easier.
  • Ensures resources are released properly.

ClassNotFoundException vs NoClassDefFoundError

Both errors relate to missing classes during runtime, but they occur in different situations.


ClassNotFoundException

ClassNotFoundException is a checked exception.

It occurs when Java attempts to load a class dynamically using methods such as:

  • Class.forName()
  • ClassLoader.loadClass()

and the class cannot be found in the classpath.

Common Causes

  • Missing JAR files.
  • Incorrect classpath.
  • Class loader issues.

Since it is a checked exception, it must be handled using a try-catch block or declared with throws.


NoClassDefFoundError

NoClassDefFoundError is an Error, not an Exception.

It occurs when:

  • The class existed during compilation.
  • The class is missing at runtime.

Common Causes

  • Required JAR removed after compilation.
  • Static initialization failure.
  • Incorrect deployment.

Since it is an Error, it usually indicates a serious application problem.


ClassNotFoundException vs NoClassDefFoundError

Feature ClassNotFoundException NoClassDefFoundError
Type Checked Exception Error
When It Occurs Class cannot be loaded dynamically Class existed during compilation but is missing at runtime
Common Cause Missing JAR or incorrect classpath Missing class after compilation or initialization failure
Handling Must be handled using try-catch Indicates a serious runtime problem

Real-Life Example

ClassNotFoundException

Trying to borrow a book that never existed in the library.

NoClassDefFoundError

The book existed when the catalog was printed but was removed from the shelf before you tried to borrow it.


Real-Time Testing Example

While executing Selenium automation scripts:

  • A missing JDBC driver JAR may cause a ClassNotFoundException.
  • A dependency that was available during compilation but missing during execution may cause a NoClassDefFoundError.

Try It: The Classic Traps in One Program

import java.util.*;

public class StringsWrappers {
    static class InvalidTestDataException extends Exception {          // checked custom exception
        InvalidTestDataException(String msg) { super(msg); }
    }

    static int parseTimeout(String raw) throws InvalidTestDataException {
        try {
            return Integer.parseInt(raw.trim());
        } catch (NumberFormatException e) {
            throw new InvalidTestDataException("timeout must be a number, got: '" + raw + "'");
        }
    }

    public static void main(String[] args) {
        String a = "Hello";
        String b = a;
        a = a + " World";                                  // creates a new String; b is unchanged
        System.out.println("a = " + a + " | b = " + b);

        String s1 = "Login", s2 = new String("Login");
        System.out.println("s1 == s2: " + (s1 == s2) + " | s1.equals(s2): " + s1.equals(s2));

        Integer x = 127, y = 127, p = 128, q = 128;       // autoboxing uses Integer.valueOf
        System.out.println("127 == 127: " + (x == y) + " | 128 == 128: " + (p == q) + " | 128 equals: " + p.equals(q));

        Set<Integer> codes = new HashSet<>(List.of(200, 201));
        System.out.println("HashSet contains Integer.valueOf(200): " + codes.contains(Integer.valueOf(200)));

        StringBuilder sb = new StringBuilder();
        for (String step : List.of("open", "login", "search")) sb.append(step).append(" > ");
        sb.setLength(sb.length() - 3);
        System.out.println("steps: " + sb);

        for (String raw : List.of(" 30 ", "3O")) {
            try {
                System.out.println("timeout = " + parseTimeout(raw));
            } catch (InvalidTestDataException e) {
                System.out.println("Caught: " + e.getMessage());
            } finally {
                System.out.println("finally runs for '" + raw + "'");
            }
        }
    }
}

Output (compiled and run with Java 21):

a = Hello World | b = Hello
s1 == s2: false | s1.equals(s2): true
127 == 127: true | 128 == 128: false | 128 equals: true
HashSet contains Integer.valueOf(200): true
steps: open > login > search
timeout = 30
finally runs for ' 30 '
Caught: timeout must be a number, got: '3O'
finally runs for '3O'
  • Immutability: changing a created a new String; b still points to "Hello".
  • == vs equals(): two Strings with the same text were not ==. Always compare text — including text read from a web page in a test — with equals().
  • The Integer cache trap: 127 == 127 was true but 128 == 128 was false, because Java caches Integer objects from −128 to 127. An assertion like actualCount == expectedCount on Integer values can pass with small numbers and fail with large ones — use equals() or compare int values.
  • hashCode/equals let a HashSet<Integer> find 200 even though it's a different object.
  • Exception handling: a custom checked exception turned a vague NumberFormatException into a clear test-data error, and finally ran in both cases.

Step through the program and watch which objects the references point to in the Java Code Visualizer.

From Real Projects

Java has been the language of every automation framework I've worked with — Selenium, TestNG, POM classes and Apache POI for Excel data. Interviews for automation roles regularly include basic programs like this one, often followed by a question on OOP concepts. Practice writing it by hand and explaining how you'd test it. On Canolog, the many forms and screens across sales, inventory, finance and service are where keeping page details in POM classes and reusable steps in a business library paid off. Compare text with equals(), never ==, especially when the text comes from a web page.


📚 Official documentation: Selenium documentation · dev.java: Learn Java (Oracle)

Encoding in Test Code: Where It Breaks

Garbled characters in tests almost always come from one place where the charset isn't set. Set it explicitly at each boundary:

WhereSet UTF-8 like this
Reading and writing filesFiles.readString(path, StandardCharsets.UTF_8)
String to bytes and backtext.getBytes(StandardCharsets.UTF_8), new String(bytes, StandardCharsets.UTF_8)
Maven source files<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> in pom.xml
REST Assured request bodies.contentType("application/json; charset=UTF-8")
MySQL JDBC URL?useUnicode=true&characterEncoding=UTF-8

Never rely on the platform default. It was the operating system's charset until Java 17 (often windows-1252 on Windows machines), and only became UTF-8 everywhere in Java 18. A test that passes on a Mac and fails on a Windows Jenkins agent is usually this.

FAQs

1. Why Are Strings Immutable in Java?

Strings are immutable because their internal array (a char[] before Java 9, a byte[] since) is private and its contents cannot be modified after the String object is created. Any modification creates a new String object.


2. What Is the Difference Between StringBuffer and StringBuilder?

Both classes create mutable strings.

  • StringBuffer is thread-safe because its methods are synchronized.
  • StringBuilder is not thread-safe but offers better performance.

3. Do Wrapper Classes Override hashCode() and equals()?

Yes.

Wrapper classes such as Integer, Character, and Boolean override both methods.

  • equals() compares values.
  • hashCode() ensures equal objects generate identical hash codes.

4. What Are the Building Blocks of Exception Handling in Java?

Java exception handling is built around:

  • try
  • catch
  • finally
  • throw
  • throws

5. What Is the Purpose of the finally Block?

The finally block always executes, whether or not an exception occurs.

It is typically used to release resources such as files, database connections, and network connections.


6. What Is the Difference Between ClassNotFoundException and NoClassDefFoundError?

  • ClassNotFoundException is a checked exception that occurs when Java cannot load a class dynamically because it is missing from the classpath.
  • NoClassDefFoundError is an Error that occurs when a class available during compilation is missing at runtime or fails during initialization.

7. What is the default charset in Java?

Since Java 18 the default charset is UTF-8 on every platform. In Java 17 and earlier it depended on the operating system, which is why code that relies on the default can behave differently on Windows and Linux.


8. How do you convert a String to UTF-8 bytes in Java?

Call text.getBytes(StandardCharsets.UTF_8), and convert back with new String(bytes, StandardCharsets.UTF_8). Using the StandardCharsets constant avoids the checked exception that the string name version throws.


9. Why does my text show characters like é instead of é?

The bytes were written as UTF-8 but read as ISO-8859-1 or windows-1252. Find the step that reads the data (file, HTTP response or database) and set its charset to UTF-8 explicitly.