Strings in Depth
Methods, immutability, and the equals() vs == trap
After this lesson, you will be able to:
- Use
charAt(),substring(),indexOf(), andlength()to inspect and extract parts of a String - Explain why
==fails for String comparison and use.equals()instead - Apply
.toUpperCase(),.toLowerCase(), and.trim()knowing that Strings are immutable - Combine String methods to parse structured text like emails and filenames
The Password That Never Matches
A student writes a login program:
Scanner scanner = new Scanner(System.in);
System.out.print("Enter password: ");
String password = scanner.nextLine();
if (password == "secret123") {
System.out.println("Access granted");
}
They type secret123. Nothing happens. They type it again, slower. Nothing. The password is correct, but the check always fails. This is the #1 bug in CS1 Java courses, and understanding why it happens requires knowing how Java thinks about Strings.
From CSCD 110: In Python,
==compares string content:"hello" == "hello"isTrue. In Java,==compares memory addresses — whether two variables point to the same object. Two Strings can have identical text but live at different memory locations. Always use.equals()for String comparison in Java.
Strings Are Reference Types
Primitive types (int, double, boolean) store their values directly in the variable. Strings are different — they are reference types. A String variable holds a reference (a pointer) to an object stored elsewhere in memory.
When you write:
String a = "hello";
String b = "hello";
Java may or may not store these at the same memory location (it depends on string interning). But when a String comes from Scanner input or concatenation, it is almost always a different object:
String a = "hello";
String b = new String("hello");
System.out.println(a == b); // false (different objects!)
System.out.println(a.equals(b)); // true (same content)
The rule is simple: always use .equals() for Strings. Save == for primitives.
| Type | Correct Comparison |
|---|---|
int, double, char |
a == b |
String |
a.equals(b) |
| Case-insensitive String | a.equalsIgnoreCase(b) |
The Trick: Put the literal on the left to avoid
NullPointerException:// If input is null, this crashes: if (input.equals("hello")) { } // NullPointerException! // This is safe — the literal is never null: if ("hello".equals(input)) { } // returns false, no crash
A student writes if (name == "Alice") and it sometimes works, sometimes doesn't. What is the correct fix?
String Indexing: charAt() and length()
Every character in a String has a zero-based index:
String: "Hello, World!"
Index: 0123456789...
H e l l o , W o r l d !
0 1 2 3 4 5 6 7 8 9 10 11 12
String s = "Hello, World!";
s.length() // 13
s.charAt(0) // 'H'
s.charAt(7) // 'W'
s.charAt(12) // '!'
Common Pitfall: The last valid index is
length() - 1. Callings.charAt(s.length())throws aStringIndexOutOfBoundsException. This is the String equivalent of an array out-of-bounds error.
Searching: indexOf() and contains()
indexOf() returns the position of the first occurrence of a substring, or -1 if not found:
String email = "student@ewu.edu";
email.indexOf("@") // 7
email.indexOf(".") // 11
email.indexOf("xyz") // -1 (not found)
email.contains("@") // true
email.startsWith("stu") // true
email.endsWith(".edu") // true
indexOf() is case-sensitive. "Hello".indexOf("hello") returns -1.
Extracting: substring()
substring() extracts part of a String. It has two forms:
String name = "Alice Wonderland";
// Two-argument: substring(start, end) — end is EXCLUSIVE
name.substring(0, 5) // "Alice" (indices 0-4)
name.substring(6, 12) // "Wonder" (indices 6-11)
// One-argument: substring(start) — from start to end of string
name.substring(6) // "Wonderland"
Key Insight: The “exclusive end” convention means
substring(start, end)returns exactlyend - startcharacters. This is consistent across Java (and most languages). Think of the indices as marking the gaps between characters.
Combining indexOf() and substring() lets you parse structured text:
String email = "student@ewu.edu";
int atIndex = email.indexOf("@");
String username = email.substring(0, atIndex); // "student"
String domain = email.substring(atIndex + 1); // "ewu.edu"
Given String s = "Java Programming";, what does s.substring(5, 8) return?
Transforming Strings (Immutability)
Strings in Java are immutable — once created, their content never changes. Every “modification” method returns a new String:
String text = " Hello, World! ";
text.toUpperCase() // " HELLO, WORLD! " (new String)
text.toLowerCase() // " hello, world! " (new String)
text.trim() // "Hello, World!" (no leading/trailing space)
The critical consequence:
String name = "alice";
name.toUpperCase(); // Returns "ALICE" — but we threw it away!
System.out.println(name); // "alice" — unchanged!
name = name.toUpperCase(); // NOW name is "ALICE"
System.out.println(name); // "ALICE"
If you do not assign the result to a variable, the new String is lost. This is one of the most common String mistakes.
| Method | What It Does | Returns |
|---|---|---|
toUpperCase() |
All letters to uppercase | new String |
toLowerCase() |
All letters to lowercase | new String |
trim() |
Removes leading/trailing whitespace | new String |
replace(old, new) |
Replaces all occurrences | new String |
String messy = " HeLLo ";
String clean = messy.trim().toLowerCase(); // "hello" — method chaining!
Complete Example: Email Parser
Here is a program that combines all the String methods to validate and parse an email address:
import java.util.Scanner;
public class EmailParser {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter your email: ");
String email = scanner.nextLine().trim().toLowerCase();
// Validate: must contain @ and .
if (!email.contains("@") || !email.contains(".")) {
System.out.println("Invalid email format.");
return;
}
int atIndex = email.indexOf("@");
int dotIndex = email.lastIndexOf(".");
// @ must come before the last .
if (atIndex > dotIndex) {
System.out.println("Invalid email format.");
return;
}
String username = email.substring(0, atIndex);
String domain = email.substring(atIndex + 1);
System.out.println("Username: " + username);
System.out.println("Domain: " + domain);
System.out.println("Length: " + email.length() + " characters");
if (domain.endsWith(".edu")) {
System.out.println("This is an educational email.");
}
}
}
Sample output:
Enter your email: Student@EWU.edu
Username: student
Domain: ewu.edu
Length: 15 characters
This is an educational email.
Notice the method chain scanner.nextLine().trim().toLowerCase() — this reads input, strips whitespace, and normalizes to lowercase in one line.
What does this code print?
String s = "hello";
s.toUpperCase();
System.out.println(s);
String s = "hello";
s.toUpperCase();
System.out.println(s);
Quick Reference: Essential String Methods
| Method | Returns | Example (s = "Hello, World!") |
Result |
|---|---|---|---|
s.length() |
int |
s.length() |
13 |
s.charAt(i) |
char |
s.charAt(0) |
'H' |
s.indexOf(str) |
int |
s.indexOf("World") |
7 |
s.contains(str) |
boolean |
s.contains(",") |
true |
s.substring(s, e) |
String |
s.substring(7, 12) |
"World" |
s.substring(s) |
String |
s.substring(7) |
"World!" |
s.toUpperCase() |
String |
s.toUpperCase() |
"HELLO, WORLD!" |
s.toLowerCase() |
String |
s.toLowerCase() |
"hello, world!" |
s.trim() |
String |
" hi ".trim() |
"hi" |
s.equals(str) |
boolean |
s.equals("Hello, World!") |
true |
s.equalsIgnoreCase(str) |
boolean |
s.equalsIgnoreCase("hello, world!") |
true |
s.startsWith(str) |
boolean |
s.startsWith("He") |
true |
s.endsWith(str) |
boolean |
s.endsWith("!") |
true |
s.replace(old, new) |
String |
s.replace("World", "Java") |
"Hello, Java!" |
Summary
Strings are reference types, not primitives. The == operator compares whether two String variables point to the same object — not whether they contain the same text. Always use .equals() for content comparison.
Strings are indexed starting at 0. Use charAt() for single characters, substring() for ranges (with exclusive end index), and indexOf() to find positions. The length() method gives the total character count, and the last valid index is length() - 1.
Strings are immutable. Every method that appears to modify a String actually returns a new one. If you forget to assign the result, the change is lost.
Next lesson: We look at Scanner patterns — how to handle the nextInt()/nextLine() buffer trap, validate input before reading, and build robust input loops.