java-foundations Lesson 7 18 min read

Strings in Depth

Methods, immutability, and the equals() vs == trap

Reading: Reges & Stepp: Ch. 3 §3.3

After this lesson, you will be able to:

  • Use charAt(), substring(), indexOf(), and length() 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" is True. 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
Check Your Understanding

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. Calling s.charAt(s.length()) throws a StringIndexOutOfBoundsException. 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 exactly end - start characters. 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"
Check Your Understanding

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.

Check Your Understanding

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.