algoz
Chapters/01 Java basics/Strings

Strings

Work with immutable text safely, navigate the pool, and avoid hidden copies.

Builds on
You'll gain
Use String without hidden costsand avoid unintentional allocations
Practice
Sheet coming sooncoding problems for this note
Summary

A Java String never changes after it is made: every "change" builds a new string, and equal literals share one object from the string pool.

The idea in real life

Think of an engraved stone nameplate hung on a house gate. Once the letters are carved into the slate, you cannot erase or alter a single letter on that plaque. If the family changes their name or corrects a spelling mistake, the stone carver does not scrape away the stone—they carve a brand-new plaque from fresh stone and hang the new one in place of the old. Java strings work the exact same way: every time you change, trim, or combine text, Java never alters the original text in memory, but produces a completely new string object.

Why it exists

  1. Storing text as raw mutable arrays of characters would allow any method with access to the array to alter the text without warning, causing bugs across unrelated parts of a program.
  2. Making strings immutable guarantees that text can be safely shared across methods, threads, and maps without defensive copying.
  3. Centralising common string operations into a standard library class eliminates the need for developers to manually write index math and buffer allocations for everyday text tasks.
  4. Reusing identical string literals through an internal pool saves heap memory across large applications where the same words and labels repeat frequently.

How it works

Creating strings

Store text using string literals, factory methods, or concatenation.

In Java, String is a reference type defined in the java.lang package. You can create strings in three primary ways:

  • String literals: Text enclosed in double quotation marks (such as "hello"). This is the form you will write most often.
  • Factory methods: The static method String.valueOf(...) converts primitive types (such as int, double, or boolean) and character arrays into string representations.
  • Concatenation with plus: When at least one operand of the + operator is a String, Java automatically converts the other operand into text and combines them into a single string, as introduced in operators.
Main.java
public class Main {
    public static void main(String[] args) {
        String greeting = "Hello";
        int score = 42;
        boolean active = true;

        String scoreText = String.valueOf(score);
        String statusText = String.valueOf(active);
        String combined = greeting + ", user! Score: " + score;

        System.out.println(scoreText);
        System.out.println(statusText);
        System.out.println(combined);
    }
}
Output
42
true
Hello, user! Score: 42

In this program:

  • greeting is assigned the string literal "Hello".
  • scoreText receives the string "42" created by String.valueOf(score).
  • statusText receives the string "true" created by String.valueOf(active).
  • combined uses the + operator to combine the string greeting, string literals, and the integer score into "Hello, user! Score: 42".

Common methods

Inspect, extract, transform, and join text using standard String methods.

The String class provides methods to inspect characters, extract substrings, search for patterns, and reformat text:

  • length(): Returns the number of 16-bit code units in the string. Notice this is a method call with parentheses (), unlike an array's length field.
  • charAt(int index): Returns the char at the specified 0-based index. Valid indices range from 0 to length() - 1.
  • substring(int beginIndex, int endIndex): Extracts a substring from beginIndex up to, but not including, endIndex. This forms a half-open interval [beginIndex, endIndex), so the length of the extracted slice is always endIndex - beginIndex. The single-argument overload substring(int beginIndex) extracts from beginIndex to the end of the string.
  • indexOf(String str): Returns the 0-based index of the first occurrence of str, or -1 if the substring is not found.
  • contains(CharSequence s): Returns true if the specified sequence appears anywhere within the string.
  • startsWith(String prefix): Returns true if the string begins with the specified prefix.
  • toUpperCase(): Returns a new string with all characters converted to uppercase.
  • trim() and strip(): Both methods remove leading and trailing whitespace. The older trim() method only removes ASCII control whitespace (code points less than or equal to \u0020). Starting in Java 11, strip() is preferred because it checks Character.isWhitespace() to remove all Unicode-defined whitespace characters.
  • split(String regex): Splits the string around matches of the specified regular expression delimiter and returns a String[] array.
  • String.join(CharSequence delimiter, CharSequence... elements): Joins multiple string elements separated by the specified delimiter.
Main.java
public class Main {
    public static void main(String[] args) {
        String text = "  Java Algorithms  ";

        int totalUnits = text.length();
        char thirdChar = text.charAt(2);
        String slice = text.substring(2, 6);
        int algoIndex = text.indexOf("Algo");
        boolean containsWord = text.contains("Algo");
        boolean starts = text.startsWith("  Java");
        String upper = text.toUpperCase();
        String trimmed = text.trim();
        String stripped = text.strip();

        String csv = "apple,banana,cherry";
        String[] fruits = csv.split(",");
        String joined = String.join(" - ", fruits);

        System.out.println("Total units: " + totalUnits);
        System.out.println("Char at index 2: " + thirdChar);
        System.out.println("Substring [2, 6): " + slice);
        System.out.println("Index of Algo: " + algoIndex);
        System.out.println("Contains Algo: " + containsWord);
        System.out.println("Starts with '  Java': " + starts);
        System.out.println("Uppercase: " + upper.trim());
        System.out.println("Trimmed: " + trimmed);
        System.out.println("Stripped: " + stripped);
        System.out.println("First split fruit: " + fruits[0]);
        System.out.println("Joined with hyphen: " + joined);
    }
}
Output
Total units: 19
Char at index 2: J
Substring [2, 6): Java
Index of Algo: 7
Contains Algo: true
Starts with '  Java': true
Uppercase: JAVA ALGORITHMS
Trimmed: Java Algorithms
Stripped: Java Algorithms
First split fruit: apple
Joined with hyphen: apple - banana - cherry

In this program:

  • totalUnits stores 19, the count of characters including leading and trailing spaces.
  • thirdChar reads the character at index 2, which is 'J'.
  • slice extracts characters from index 2 to index 5, producing "Java".
  • algoIndex finds the starting index of "Algo", which is 7.
  • containsWord verifies that "Algo" exists within text.
  • starts checks whether text begins with " Java".
  • upper holds the uppercase version of text.
  • trimmed and stripped remove the outer spaces, producing "Java Algorithms".
  • fruits is a three-element array produced by splitting csv on commas.
  • joined recombines the array elements using " - " as a separator.

Comparing strings

Compare text content with equals and compareTo rather than double equals.

Comparing strings requires distinguishing between object identity and value equality:

  • ==: Checks reference identity—whether two variables point to the exact same object in heap memory. It does not inspect the characters inside the string.
  • equals(Object anObject): Compares the actual character sequences. Returns true if both strings contain the same characters in the exact same order.
  • equalsIgnoreCase(String anotherString): Compares character sequences while ignoring differences in letter casing.
  • compareTo(String anotherString): Compares two strings character by character according to their Unicode values.

The integer returned by compareTo indicates relative order:

  • Returns 0 if both strings are equal.
  • Returns a negative number if the calling string comes before the argument alphabetically.
  • Returns a positive number if the calling string comes after the argument alphabetically.

If the strings differ at an index, compareTo returns the difference thisChar - otherChar at the first mismatch. If one string is a prefix of the other, it returns the difference in their lengths (this.length() - anotherString.length()).

Main.java
public class Main {
    public static void main(String[] args) {
        String word1 = "apple";
        String word2 = "apple";
        String word3 = new String("apple");
        String word4 = "Apple";

        System.out.println("word1 == word2: " + (word1 == word2));
        System.out.println("word1 == word3: " + (word1 == word3));
        System.out.println("word1.equals(word3): " + word1.equals(word3));
        System.out.println("word1.equalsIgnoreCase(word4): " + word1.equalsIgnoreCase(word4));

        int compareEqual = "apple".compareTo("apple");
        int compareLess = "apple".compareTo("banana");
        int compareGreater = "banana".compareTo("apple");

        System.out.println("apple vs apple: " + compareEqual);
        System.out.println("apple vs banana: " + compareLess);
        System.out.println("banana vs apple: " + compareGreater);
    }
}
Output
word1 == word2: true
word1 == word3: false
word1.equals(word3): true
word1.equalsIgnoreCase(word4): true
apple vs apple: 0
apple vs banana: -1
banana vs apple: 1

In this program:

  • word1 == word2 evaluates to true because both literals share the same pooled instance.
  • word1 == word3 evaluates to false because new String allocates a distinct object on the heap.
  • word1.equals(word3) evaluates to true because both strings hold identical characters.
  • word1.equalsIgnoreCase(word4) evaluates to true because "apple" and "Apple" differ only in casing.
  • "apple".compareTo("banana") produces -1 because 'a' (ASCII 97) minus 'b' (ASCII 98) equals -1.
  • "banana".compareTo("apple") produces 1 because 'b' minus 'a' equals 1.

Characters in a string

Iterate over characters and map letter frequencies using index arithmetic.

You can examine characters in a string by indexing with a loop or converting the string to an array:

  • charAt(i): Allows you to iterate from i = 0 to i < s.length() without allocating extra heap memory.
  • s.toCharArray(): Allocates a new char[] array containing a copy of all characters in the string. This enables for-each loops and in-place array manipulation.
  • Character arithmetic with c - 'a': In problems dealing with lowercase English letters, subtracting 'a' maps 'a' to index 0, 'b' to index 1, and 'z' to index 25. As explained in casting and type promotion, subtracting two char values promotes both operands to 32-bit integers, allowing the result to serve directly as an index into an integer array of size 26.
Main.java
public class Main {
    public static void main(String[] args) {
        String text = "banana";

        int vowelCount = 0;
        for (int i = 0; i < text.length(); i++) {
            char current = text.charAt(i);
            if (current == 'a') {
                vowelCount++;
            }
        }
        System.out.println("Occurrences of 'a': " + vowelCount);

        char[] letters = text.toCharArray();
        System.out.println("First char from array: " + letters[0]);

        int[] frequencies = new int[26];
        for (int i = 0; i < text.length(); i++) {
            char letter = text.charAt(i);
            int index = letter - 'a';
            frequencies[index]++;
        }

        System.out.println("Count for 'a': " + frequencies['a' - 'a']);
        System.out.println("Count for 'b': " + frequencies['b' - 'a']);
        System.out.println("Count for 'n': " + frequencies['n' - 'a']);
    }
}
Output
Occurrences of 'a': 3
First char from array: b
Count for 'a': 3
Count for 'b': 1
Count for 'n': 2

In this program:

  • vowelCount counts occurrences of 'a' by iterating over indices from 0 to 5 using charAt.
  • letters is a newly allocated char[] created by toCharArray().
  • frequencies is an integer array of size 26 storing counts for letters 'a' through 'z'.
  • letter - 'a' maps 'a' to index 0, 'b' to index 1, and 'n' to index 13.

Going deeper

Immutability

Strings cannot be altered in place; every operation returns a fresh object.

In Java, String objects are immutable. Once a string is created on the heap, its internal characters and length cannot be modified.

Every method in the String class that appears to alter text—such as toUpperCase(), replace(), substring(), or trim()—does not touch the existing string. Instead, it constructs and returns a brand-new String object containing the modified content.

If you invoke a transformation method and discard its return value, your program performs the allocation work but leaves the original variable untouched.

Main.java
public class Main {
    public static void main(String[] args) {
        String original = "hello";

        // Calling toUpperCase without assigning the result
        original.toUpperCase();
        System.out.println("After unassigned call: " + original);

        // Assigning the newly created string to a variable
        String transformed = original.toUpperCase();
        System.out.println("Transformed string: " + transformed);
        System.out.println("Original remains: " + original);
    }
}
Output
After unassigned call: hello
Transformed string: HELLO
Original remains: hello

In this program:

  • original.toUpperCase() creates a new string "HELLO", but because the result is not assigned, it is immediately discarded. original continues pointing to "hello".
  • Assigning original.toUpperCase() to transformed stores the reference to the newly created string, while original remains unchanged.

The string pool

The JVM shares identical string literals in a central heap pool.

To conserve heap memory, the Java Virtual Machine maintains a specialized table in heap memory known as the string pool (or intern table).

When the JVM loads class files and encounters string literals (such as "cat"), it checks the pool:

  • If an identical string already exists in the pool, the JVM points the variable to that existing object.
  • If the string is not yet in the pool, the JVM instantiates a new string object, registers it in the pool, and returns its reference.

Because of this sharing, two variables initialised with the same literal point to the same String object (literalOne == literalTwo evaluates to true).

By contrast, using the new keyword explicitly commands the JVM to allocate a separate string object on the heap outside the pool, bypassing literal deduplication.

The intern() method allows you to query the pool manually: it searches for an equal string in the pool. If found, it returns the pooled reference; if not found, it adds the current string to the pool and returns its reference.

Main.java
public class Main {
    public static void main(String[] args) {
        String literalOne = "cat";
        String literalTwo = "cat";
        String constructed = new String("cat");
        String interned = constructed.intern();

        System.out.println("literalOne == literalTwo: " + (literalOne == literalTwo));
        System.out.println("literalOne == constructed: " + (literalOne == constructed));
        System.out.println("literalOne.equals(constructed): " + literalOne.equals(constructed));
        System.out.println("literalOne == interned: " + (literalOne == interned));
    }
}
Output
literalOne == literalTwo: true
literalOne == constructed: false
literalOne.equals(constructed): true
literalOne == interned: true

In this program:

  • literalOne == literalTwo is true because both point to the shared string in the string pool.
  • literalOne == constructed is false because new String allocates a distinct object on the heap.
  • literalOne.equals(constructed) is true because their character content is identical.
  • literalOne == interned is true because constructed.intern() returns the pooled reference matching "cat".

Never rely on == for text comparisons in real programs. Always use equals(), because strings arriving from user input, network sockets, or string manipulations are distinct heap objects.

Concatenation in a loop

Repeated concatenation in a loop copies text quadratically.

When you combine strings on a single line using the + operator, the Java compiler optimizes the expression into a single-pass construction.

However, using the += operator inside a loop repeatedly allocates intermediate objects. Because strings are immutable, Java cannot append characters to the end of an existing string buffer. In each iteration, Java must:

  1. Allocate a brand-new string on the heap large enough to hold the previous text plus the new segment.
  2. Copy every character from the old string into the new string.
  3. Append the new characters.
  4. Update the reference variable to point to the new string.
Main.java
public class Main {
    public static void main(String[] args) {
        String numbers = "";
        for (int i = 0; i < 5; i++) {
            numbers += i;
        }
        System.out.println("Result: " + numbers);
    }
}
Output
Result: 01234

In this program:

  • In iteration 0, copying 0 characters and adding "0".
  • In iteration 1, copying "0" (1 character) and adding "1".
  • In iteration 2, copying "01" (2 characters) and adding "2".
  • In iteration 3, copying "012" (3 characters) and adding "3".
  • In iteration 4, copying "0123" (4 characters) and adding "4".

Across a loop of n iterations, the total number of characters copied into memory is: 1 + 2 + 3 + ... + (n - 1) = n * (n - 1) / 2

This means repeated concatenation performs work proportional to n * n (quadratic work). Building a 10,000-character string this way copies roughly 50,000,000 characters into temporary objects. In note 11, you will learn how StringBuilder avoids these repeated copies by using an expandable internal buffer.

Text blocks

Write multi-line strings cleanly using triple quotes introduced in Java 15.

Writing multi-line strings in earlier versions of Java required escaped newline characters \n and chained string concatenations.

Starting in Java 15 (standardized under JEP 378) and available on JDK 17, Java provides text blocks. A text block is a multi-line string literal delimited by three double quotes """.

Key rules for text blocks:

  • The opening """ must be followed immediately by a line terminator; text content cannot begin on the same line as the opening triple quotes.
  • The closing """ marks the end of the text block.
  • Java strips incidental whitespace—the common leading indentation shared by all lines—while preserving relative indentation and newline breaks.
  • Double quotes inside text blocks do not require escaping unless you write three consecutive double quotes.
Main.java
public class Main {
    public static void main(String[] args) {
        String query = """
                SELECT id, username, score
                FROM leaderboard
                WHERE score >= 100
                ORDER BY score DESC;
                """;

        System.out.println(query.trim());
    }
}
Output
SELECT id, username, score
FROM leaderboard
WHERE score >= 100
ORDER BY score DESC;

In this program:

  • query holds a multi-line SQL statement formatted across four lines.
  • The compiler strips the shared leading indentation, producing clean multi-line text without manual concatenation.

Under the hood

How a String is stored

Compact strings store Latin-1 characters as single bytes to halve memory.

Before Java 9, every java.lang.String object stored its characters in an internal char[] value array. Because each Java char consumes 16 bits (2 bytes), an English string like "algorithm" required 2 bytes for every character, even though the upper 8 bits of each character were zeros.

In JDK 9, JEP 254 introduced Compact Strings to reduce heap consumption. Inside the JVM, the internal structure of String changed from a character array to:

  • private final byte[] value;
  • private final byte coder;

The coder field is an internal flag with two possible values:

  • LATIN1 (value 0): Used when every character in the string has a code point of 0xFF or lower (ISO-8859-1 / Latin-1 encoding, covering standard ASCII English text). Each character occupies exactly 1 byte of heap memory.
  • UTF16 (value 1): Used whenever at least one character in the string has a code point greater than 0xFF (such as Devanagari, Cyrillic, Chinese, Japanese, or emoji characters). Each character occupies 2 bytes of heap memory.

Compact strings roughly halve the heap memory used by strings in applications with predominantly Latin-1 text, without altering the public String API or behaviour.

char and Unicode

UTF-16 code units differ from full Unicode code points for supplementary characters.

Java's char type is an unsigned 16-bit integer representing values from \u0000 to \uFFFF (decimal 0 to 65,535).

When Java was created in 1995, Unicode was expected to fit entirely within 16 bits (65,536 characters), known as the Basic Multilingual Plane (BMP). Over time, Unicode expanded to define more than 149,000 characters spanning from code point U+0000 up to U+10FFFF.

Characters with code points above U+FFFF are called supplementary characters. These include emojis, historical scripts, and specialized mathematical symbols. Because a 16-bit char cannot hold values above 65,535, Java uses UTF-16 encoding:

  • A supplementary character is stored as a surrogate pair: two consecutive 16-bit code units (a high surrogate in the range \uD800 to \uDBFF, followed by a low surrogate in the range \uDC00 to \uDFFF).
  • length() returns the number of 16-bit UTF-16 code units, not the number of user-visible characters.
  • codePointCount(int beginIndex, int endIndex) counts the actual number of Unicode code points across that index span.
  • charAt(int index) returns a single 16-bit code unit. If that index lands on a surrogate pair, it returns only half of the character.
  • codePointAt(int index) returns the full 32-bit integer code point starting at index.
Main.java
public class Main {
    public static void main(String[] args) {
        String emoji = "\uD83D\uDE00";

        int codeUnits = emoji.length();
        int codePoints = emoji.codePointCount(0, emoji.length());
        int firstUnit = (int) emoji.charAt(0);
        int fullCodePoint = emoji.codePointAt(0);

        System.out.println("UTF-16 code units (length): " + codeUnits);
        System.out.println("Unicode code points: " + codePoints);
        System.out.println("First code unit: " + firstUnit);
        System.out.println("Code point value: " + fullCodePoint);
    }
}
Output
UTF-16 code units (length): 2
Unicode code points: 1
First code unit: 55357
Code point value: 128512

In this program:

  • emoji contains the grinning face emoji (Unicode code point U+1F600, decimal 128512), represented by surrogate pair \uD83D\uDE00.
  • emoji.length() returns 2 because the emoji requires two 16-bit code units.
  • emoji.codePointCount(0, emoji.length()) returns 1 because the surrogate pair forms a single Unicode code point.
  • emoji.charAt(0) returns the high surrogate \uD83D (decimal 55357).
  • emoji.codePointAt(0) inspects both surrogates and returns the full 32-bit code point 128512.

hashCode

String caches its polynomial 31 hash code after the first calculation.

The hash code of a String is computed using a deterministic polynomial formula defined in the Java specification: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

In this formula:

  • s[i] represents the 16-bit numeric value of the character at index i.
  • n is the length of the string.
  • The multiplier 31 is an odd prime. Multiplying by 31 can be optimized by compilers into a bit shift and subtraction: (x << 5) - x.

Because strings are immutable, a string's character sequence never changes after construction. The String class maintains an internal field private int hash; (defaulting to 0) that caches the computed hash code.

The first time hashCode() is invoked, Java iterates over the characters, calculates the polynomial sum, and stores the integer in hash. Every subsequent call to hashCode() on that object immediately returns the cached value in constant time without re-examining the characters.

In chapter 04, you will see how HashMap and HashSet use this cached hash code to find where a key is stored.

Main.java
public class Main {
    public static void main(String[] args) {
        String word = "cat";
        int builtInHash = word.hashCode();

        // Formula: s[0]*31^2 + s[1]*31^1 + s[2]*31^0
        int manualHash = ('c' * 31 * 31) + ('a' * 31) + 't';

        System.out.println("Built-in hashCode: " + builtInHash);
        System.out.println("Manual calculation: " + manualHash);
        System.out.println("Match: " + (builtInHash == manualHash));
    }
}
Output
Built-in hashCode: 98262
Manual calculation: 98262
Match: true

In this program:

  • builtInHash retrieves the hash code computed by String.hashCode().
  • manualHash computes the exact polynomial sum using ASCII values 'c' (99), 'a' (97), and 't' (116).
  • Both values equal 98262, confirming the calculation formula.

How + compiles

Modern Java compiles string concatenation using invokedynamic and StringConcatFactory.

Before Java 9, compiling string concatenation expressions with the + operator caused javac to emit bytecode that instantiated a StringBuilder, chained .append() calls for each operand, and invoked .toString(). While this avoided quadratic copying on a single line of code, hard-coding StringBuilder into bytecode locked the JVM into a rigid allocation strategy.

Since JDK 9, JEP 280 (Indify String Concatenation) replaced bytecode StringBuilder chains with the invokedynamic instruction.

Consider a class that combines a string with an integer:

Java
public class ConcatDemo {
    public static String buildGreeting(String name, int score) {
        return "Player " + name + " scored " + score;
    }
}

Disassembling the compiled class with javap -c ConcatDemo reveals how JDK 17 compiles this method:

text
Compiled from "ConcatDemo.java"
public class ConcatDemo {
  public static java.lang.String buildGreeting(java.lang.String, int);
    Code:
       0: aload_0
       1: iload_1
       2: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;I)Ljava/lang/String;
       7: areturn
}

The bytecode above was produced directly with JDK 17 (comments starting with // were generated by javap to describe each instruction):

  • aload_0: Loads the first method parameter (name) onto the operand stack.
  • iload_1: Loads the second method parameter (score) onto the operand stack.
  • invokedynamic: Calls StringConcatFactory.makeConcatWithConstants to perform concatenation at runtime.
  • areturn: Returns the completed String reference to the caller.

At runtime, the JVM inspects the recipe and argument types. It can determine the exact byte size needed, allocate a single byte array, and copy the bytes directly into place, completely eliminating intermediate object allocations.

In Java

Here is a complete program demonstrating string inspection, sanitization, extraction, frequency counting, and delimiter formatting:

In this program:

  • cleanInput: Strips outer whitespace and converts text to lowercase.
  • extractDomain: Extracts the domain part of an email address following the @ symbol.
  • maskCardNumber: Masks all characters with asterisks except for the final four digits.
  • countFrequencies: Counts lowercase letter frequencies into an integer array of size 26.
  • reformatDelimiters: Splits a comma-separated list, trims each item, and joins them using pipe delimiters.
Main.java
public class Main {
    // Returns the sanitized string stripped of outer whitespace and converted to lowercase.
    static String cleanInput(String raw) {
        return raw.strip().toLowerCase();
    }

    // Returns the domain part of an email address following the @ symbol.
    static String extractDomain(String email) {
        int atIndex = email.indexOf("@");
        if (atIndex == -1) {
            return "";
        }
        return email.substring(atIndex + 1);
    }

    // Returns the card number masked with asterisks except for the last four digits.
    static String maskCardNumber(String cardNumber) {
        int length = cardNumber.length();
        if (length <= 4) {
            return cardNumber;
        }
        int visibleStart = length - 4;
        String visiblePart = cardNumber.substring(visibleStart);
        String maskedPrefix = "*".repeat(visibleStart);
        return maskedPrefix + visiblePart;
    }

    // Returns an array of 26 integers with letter frequencies for lowercase English letters.
    static int[] countFrequencies(String text) {
        int[] frequencies = new int[26];
        for (int i = 0; i < text.length(); i++) {
            char current = text.charAt(i);
            if (current >= 'a' && current <= 'z') {
                frequencies[current - 'a']++;
            }
        }
        return frequencies;
    }

    // Formats a comma-separated list of items into a clean pipe-delimited string.
    static String reformatDelimiters(String csv) {
        String[] items = csv.split(",");
        for (int i = 0; i < items.length; i++) {
            items[i] = items[i].trim();
        }
        return String.join(" | ", items);
    }

    public static void main(String[] args) {
        String rawEmail = "  Alice@Company.org  ";
        String cleanedEmail = cleanInput(rawEmail);
        String domain = extractDomain(cleanedEmail);

        String card = "4111123456789012";
        String masked = maskCardNumber(card);

        String csvTags = "java, algorithms, data-structures";
        String formattedTags = reformatDelimiters(csvTags);

        int[] frequencies = countFrequencies("banana");

        System.out.println("Cleaned email: " + cleanedEmail);
        System.out.println("Domain: " + domain);
        System.out.println("Masked card: " + masked);
        System.out.println("Formatted tags: " + formattedTags);
        System.out.println("Frequency of 'a': " + frequencies['a' - 'a']);
        System.out.println("Frequency of 'n': " + frequencies['n' - 'a']);
    }
}
Output
Cleaned email: alice@company.org
Domain: company.org
Masked card: ************9012
Formatted tags: java | algorithms | data-structures
Frequency of 'a': 3
Frequency of 'n': 2

Traps

  1. Comparing strings with double equals instead of equals

    The == operator checks reference identity, verifying whether two variables point to the exact same object in heap memory. Strings produced at runtime or created with new String reside at different addresses even when their characters are identical. Always compare text content using equals() or equalsIgnoreCase().

    ✕ WRONGif (name == "admin")
    ✓ RIGHTif (name.equals("admin"))
  2. Ignoring the return value of string transformation methods

    Java strings are immutable, meaning methods such as toUpperCase(), trim(), and replace() never alter the existing string object. They construct and return a new string containing the modified text. If you discard the return value without assigning it to a variable, the original string remains completely unchanged.

    ✕ WRONGtext.toUpperCase();
    ✓ RIGHTtext = text.toUpperCase();
  3. Treating the second substring argument as a length or inclusive index

    In substring(beginIndex, endIndex), endIndex is exclusive, forming a half-open interval [beginIndex, endIndex). To extract a substring of length k starting at index start, pass start + k as the second argument. Passing 4 for a five-character stretch stops at index 3 and produces only four characters.

    ✕ WRONGString slice = text.substring(0, 4);
    ✓ RIGHTString slice = text.substring(0, 5);
  4. Splitting by unescaped dot or special regular expression characters

    The split method accepts a regular expression rather than a literal delimiter. In regular expressions, a single dot . matches any character, causing split(".") to split on every single character and return an empty array. Escape regex metacharacters using double backslashes \\. or wrap the delimiter with Pattern.quote(".").

    ✕ WRONGString[] parts = filename.split(".");
    ✓ RIGHTString[] parts = filename.split("\\.");
  5. Concatenating strings using plus-equals inside a loop

    Each execution of += constructs a brand-new string by copying all characters accumulated so far. Across a loop of n iterations, repeated concatenation copies about n * n / 2 characters, resulting in quadratic work and excessive memory allocation. Use StringBuilder (covered in note 11) to accumulate characters in a single reusable buffer.

    ✕ WRONGfor (int i = 0; i < n; i++) text += items[i];
    ✓ RIGHTStringBuilder builder = new StringBuilder(); for (int i = 0; i < n; i++) builder.append(items[i]);
  6. Reading the character at the string length index

    String indices in Java are 0-based, beginning at 0 and ending at text.length() - 1. Passing text.length() as an index exceeds the valid bounds and throws StringIndexOutOfBoundsException. Always subtract one to access the final character of a non-empty string.

    ✕ WRONGchar last = text.charAt(text.length());
    ✓ RIGHTchar last = text.charAt(text.length() - 1);
  7. Adding two char variables expecting string concatenation

    In Java, adding two char variables with the + operator applies binary numeric promotion, converting both characters to 32-bit integers and summing their Unicode numeric values. The result is an int rather than combined text. Prepend an empty string literal "" + or call String.valueOf() to trigger string concatenation instead of numeric addition.

    ✕ WRONGString code = firstInitial + secondInitial;
    ✓ RIGHTString code = "" + firstInitial + secondInitial;
  8. Assuming length counts visible characters for supplementary Unicode text

    The length() method returns the number of 16-bit UTF-16 code units stored in the string, not the number of user-perceived characters. Characters outside the Basic Multilingual Plane (such as emojis and certain mathematical symbols) require two code units as a surrogate pair, causing length() to return 2 for a single character. Call codePointCount(0, length()) when you need the exact number of Unicode code points.

    ✕ WRONGint charCount = emoji.length();
    ✓ RIGHTint charCount = emoji.codePointCount(0, emoji.length());
AlgozSet in Geist & Geist MonoJava track