01 · Java basics — note 10 of 12
Strings
Work with immutable text safely, navigate the pool, and avoid hidden copies.
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
- 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.
- Making strings immutable guarantees that text can be safely shared across methods, threads, and maps without defensive copying.
- 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.
- 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 asint,double, orboolean) and character arrays into string representations. - Concatenation with plus: When at least one operand of the
+operator is aString, Java automatically converts the other operand into text and combines them into a single string, as introduced in operators.
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);
}
}42
true
Hello, user! Score: 42In this program:
greetingis assigned the string literal"Hello".scoreTextreceives the string"42"created byString.valueOf(score).statusTextreceives the string"true"created byString.valueOf(active).combineduses the+operator to combine the stringgreeting, string literals, and the integerscoreinto"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'slengthfield.charAt(int index): Returns thecharat the specified 0-based index. Valid indices range from0tolength() - 1.substring(int beginIndex, int endIndex): Extracts a substring frombeginIndexup to, but not including,endIndex. This forms a half-open interval[beginIndex, endIndex), so the length of the extracted slice is alwaysendIndex - beginIndex. The single-argument overloadsubstring(int beginIndex)extracts frombeginIndexto the end of the string.indexOf(String str): Returns the 0-based index of the first occurrence ofstr, or-1if the substring is not found.contains(CharSequence s): Returnstrueif the specified sequence appears anywhere within the string.startsWith(String prefix): Returnstrueif the string begins with the specified prefix.toUpperCase(): Returns a new string with all characters converted to uppercase.trim()andstrip(): Both methods remove leading and trailing whitespace. The oldertrim()method only removes ASCII control whitespace (code points less than or equal to\u0020). Starting in Java 11,strip()is preferred because it checksCharacter.isWhitespace()to remove all Unicode-defined whitespace characters.split(String regex): Splits the string around matches of the specified regular expression delimiter and returns aString[]array.String.join(CharSequence delimiter, CharSequence... elements): Joins multiple string elements separated by the specified delimiter.
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);
}
}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 - cherryIn this program:
totalUnitsstores 19, the count of characters including leading and trailing spaces.thirdCharreads the character at index 2, which is'J'.sliceextracts characters from index 2 to index 5, producing"Java".algoIndexfinds the starting index of"Algo", which is 7.containsWordverifies that"Algo"exists withintext.startschecks whethertextbegins with" Java".upperholds the uppercase version oftext.trimmedandstrippedremove the outer spaces, producing"Java Algorithms".fruitsis a three-element array produced by splittingcsvon commas.joinedrecombines 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. Returnstrueif 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
0if 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()).
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);
}
}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: 1In this program:
word1 == word2evaluates totruebecause both literals share the same pooled instance.word1 == word3evaluates tofalsebecausenew Stringallocates a distinct object on the heap.word1.equals(word3)evaluates totruebecause both strings hold identical characters.word1.equalsIgnoreCase(word4)evaluates totruebecause"apple"and"Apple"differ only in casing."apple".compareTo("banana")produces-1because'a'(ASCII 97) minus'b'(ASCII 98) equals -1."banana".compareTo("apple")produces1because'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 fromi = 0toi < s.length()without allocating extra heap memory.s.toCharArray(): Allocates a newchar[]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 twocharvalues promotes both operands to 32-bit integers, allowing the result to serve directly as an index into an integer array of size 26.
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']);
}
}Occurrences of 'a': 3
First char from array: b
Count for 'a': 3
Count for 'b': 1
Count for 'n': 2In this program:
vowelCountcounts occurrences of'a'by iterating over indices from 0 to 5 usingcharAt.lettersis a newly allocatedchar[]created bytoCharArray().frequenciesis 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.
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);
}
}After unassigned call: hello
Transformed string: HELLO
Original remains: helloIn this program:
original.toUpperCase()creates a new string"HELLO", but because the result is not assigned, it is immediately discarded.originalcontinues pointing to"hello".- Assigning
original.toUpperCase()totransformedstores the reference to the newly created string, whileoriginalremains 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.
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));
}
}literalOne == literalTwo: true
literalOne == constructed: false
literalOne.equals(constructed): true
literalOne == interned: trueIn this program:
literalOne == literalTwoistruebecause both point to the shared string in the string pool.literalOne == constructedisfalsebecausenew Stringallocates a distinct object on the heap.literalOne.equals(constructed)istruebecause their character content is identical.literalOne == internedistruebecauseconstructed.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:
- Allocate a brand-new string on the heap large enough to hold the previous text plus the new segment.
- Copy every character from the old string into the new string.
- Append the new characters.
- Update the reference variable to point to the new string.
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);
}
}Result: 01234In 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.
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());
}
}SELECT id, username, score
FROM leaderboard
WHERE score >= 100
ORDER BY score DESC;In this program:
queryholds 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 of0xFFor 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 than0xFF(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
\uD800to\uDBFF, followed by a low surrogate in the range\uDC00to\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 atindex.
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);
}
}UTF-16 code units (length): 2
Unicode code points: 1
First code unit: 55357
Code point value: 128512In this program:
emojicontains the grinning face emoji (Unicode code pointU+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 indexi.nis 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.
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));
}
}Built-in hashCode: 98262
Manual calculation: 98262
Match: trueIn this program:
builtInHashretrieves the hash code computed byString.hashCode().manualHashcomputes 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:
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:
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: CallsStringConcatFactory.makeConcatWithConstantsto perform concatenation at runtime.areturn: Returns the completedStringreference 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.
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']);
}
}Cleaned email: alice@company.org
Domain: company.org
Masked card: ************9012
Formatted tags: java | algorithms | data-structures
Frequency of 'a': 3
Frequency of 'n': 2Traps
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 withnew Stringreside at different addresses even when their characters are identical. Always compare text content usingequals()orequalsIgnoreCase().✕ WRONGif (name == "admin")✓ RIGHTif (name.equals("admin"))Ignoring the return value of string transformation methods
Java strings are immutable, meaning methods such as
toUpperCase(),trim(), andreplace()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();Treating the second substring argument as a length or inclusive index
In
substring(beginIndex, endIndex),endIndexis exclusive, forming a half-open interval[beginIndex, endIndex). To extract a substring of lengthkstarting at indexstart, passstart + kas the second argument. Passing4for 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);Splitting by unescaped dot or special regular expression characters
The
splitmethod accepts a regular expression rather than a literal delimiter. In regular expressions, a single dot.matches any character, causingsplit(".")to split on every single character and return an empty array. Escape regex metacharacters using double backslashes\\.or wrap the delimiter withPattern.quote(".").✕ WRONGString[] parts = filename.split(".");✓ RIGHTString[] parts = filename.split("\\.");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 ofniterations, repeated concatenation copies aboutn * n / 2characters, resulting in quadratic work and excessive memory allocation. UseStringBuilder(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]);Reading the character at the string length index
String indices in Java are 0-based, beginning at
0and ending attext.length() - 1. Passingtext.length()as an index exceeds the valid bounds and throwsStringIndexOutOfBoundsException. 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);Adding two char variables expecting string concatenation
In Java, adding two
charvariables with the+operator applies binary numeric promotion, converting both characters to 32-bit integers and summing their Unicode numeric values. The result is anintrather than combined text. Prepend an empty string literal"" +or callString.valueOf()to trigger string concatenation instead of numeric addition.✕ WRONGString code = firstInitial + secondInitial;✓ RIGHTString code = "" + firstInitial + secondInitial;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, causinglength()to return 2 for a single character. CallcodePointCount(0, length())when you need the exact number of Unicode code points.✕ WRONGint charCount = emoji.length();✓ RIGHTint charCount = emoji.codePointCount(0, emoji.length());