Skip to content

Latest commit

 

History

History
546 lines (401 loc) · 14.7 KB

File metadata and controls

546 lines (401 loc) · 14.7 KB

Understanding Strings in Java

A lecture for adult learners — this is the reading to work through before java-strings-exercises.md. Its goal isn't to make you a Java expert; it's to give you enough working knowledge of the most common String methods that you can read a chunk of Java code someone (or some AI agent) wrote and judge, with confidence, whether it does what it claims to do.

We won't cover every method Java has for Strings — just the ones you'll actually run into in real code (and in the exercises that follow this lecture).


1. What Is a String, and Why Does "Immutable" Matter?

A String in Java is a sequence of characters — "hello", "42", "" (an empty string), all Strings. You've probably already used them without thinking too hard about it.

The one idea you need to internalize before anything else: Java Strings never change once created. Every method that looks like it's "modifying" a string — making it uppercase, trimming whitespace, replacing characters — is lying to you a little. What it's actually doing is building a brand-new string with the result, and handing that new string back to you. The original string is untouched, forever.

This means code like this does nothing useful:

String name = "  ana  ";
name.trim();          // creates a new trimmed string... and throws it away
System.out.println(name); // still prints "  ana  "

The new string has to be captured — usually by reassigning the variable:

String name = "  ana  ";
name = name.trim();   // capture the result
System.out.println(name); // prints "ana"

Every method below follows this rule: it returns a new value; it never changes the string you called it on. Keep that in mind and half of Java's String "gotchas" stop being surprising.


2. Comparing Strings

.equals(other)

Checks whether two strings hold the same characters. This is almost always what you want when asking "are these two strings the same?"

When to use it: any time you're checking whether string content matches — usernames, commands, filenames, anything a human typed or a file produced.

String a = "hello";
String b = "hel" + "lo"; // built at runtime
boolean same = a.equals(b); // true — same characters
if (command.equals("quit")) {
    System.out.println("Goodbye!");
}

Why not ==? == checks whether two variables point to the exact same object in memory, not whether they hold the same text. It's a common mistake to reach for == here — always use .equals() for content comparison.

.equalsIgnoreCase(other)

Same idea as .equals(), but treats uppercase and lowercase letters as equivalent.

When to use it: matching user input where case shouldn't matter — e.g. a yes/no prompt, a search term, a case-insensitive command.

String answer = "YES";
boolean confirmed = answer.equalsIgnoreCase("yes"); // true
String fileExt = "TXT";
if (fileExt.equalsIgnoreCase("txt")) {
    System.out.println("It's a text file.");
}

.compareTo(other)

Returns a negative number if the calling string comes before other alphabetically, 0 if they're equal, and a positive number if it comes after. It's the method sorting relies on.

When to use it: sorting strings, or checking alphabetical order.

"apple".compareTo("banana"); // negative — "apple" comes first
public static boolean comesBefore(String a, String b) {
    return a.compareTo(b) < 0;
}

Objects.equals(a, b)

A null-safe version of .equals(). Regular .equals() throws a NullPointerException if you call it on a null reference. Objects.equals sidesteps that by checking for null first.

When to use it: whenever either value being compared might legitimately be null — e.g. an optional field that was never set.

import java.util.Objects;

Objects.equals("cat", "cat");   // true
Objects.equals(null, null);     // true — both null counts as equal
Objects.equals(null, "cat");    // false — no exception thrown
public static boolean sameOrBothNull(String a, String b) {
    return Objects.equals(a, b);
}

3. Extracting Parts of a String

.length()

Returns the number of characters in the string.

When to use it: anywhere you need to know "how long is this," including as the boundary for a loop or a substring() call.

"hello".length(); // 5
public static boolean isLongEnough(String password) {
    return password.length() >= 8;
}

.charAt(index)

Returns the single character at a given position. Indexing starts at 0, and the last valid index is length() - 1.

When to use it: inspecting or processing one character at a time — e.g. checking the first letter, or looping character-by-character.

"hello".charAt(0); // 'h'
public static void printChars(String s) {
    for (int i = 0; i < s.length(); i++) {
        System.out.println(s.charAt(i));
    }
}

.substring(begin) and .substring(begin, end)

Returns a piece of the string. The one-argument form returns everything from begin to the end. The two-argument form returns everything from begin up to — but not includingend.

When to use it: pulling out a portion of text you already know the position of — a file extension, the first few characters, everything after a prefix.

"hello world".substring(6);      // "world"  (from index 6 to the end)
"hello world".substring(0, 5);   // "hello"  (index 0 up to, not including, 5)
public static String lastThree(String s) {
    return s.substring(s.length() - 3); // last 3 characters
}

Watch the boundary. end is exclusive — this trips people up constantly. "hello".substring(0, 3) gives "hel", not "hell".

.indexOf(x) and .lastIndexOf(x)

indexOf finds the position of the first occurrence of a character or substring; lastIndexOf finds the last occurrence. Both return -1 if there's no match.

When to use it: locating where something is inside a string — e.g. the position of a file extension's ., or checking that a character exists at all (indexOf(x) >= 0).

"report.final.pdf".indexOf('.');      // 6  (the first dot)
"report.final.pdf".lastIndexOf('.');  // 12 (the last dot)
public static String extension(String filename) {
    int dot = filename.lastIndexOf('.');
    if (dot == -1) {
        return "";
    }
    return filename.substring(dot + 1);
}

4. Searching Strings

.contains(substring)

Returns true if the substring appears anywhere in the string.

When to use it: a simple "does this text show up somewhere in there?" check, when you don't care where.

"Hello World".contains("World"); // true
public static boolean containsWord(String text, String word) {
    return text.toLowerCase().contains(word.toLowerCase()); // case-insensitive
}

.startsWith(prefix) and .endsWith(suffix)

Check whether the string begins (or ends) with a specific piece of text.

When to use it: validating a format — URLs, filenames, command prefixes — where the position of the match matters, not just its presence.

"https://example.com".startsWith("https://"); // true
public static boolean isSecure(String url) {
    return url.startsWith("https://");
}
"weekend".endsWith("end"); // true

5. Case, Whitespace & Trimming

.toUpperCase() and .toLowerCase()

Return a new copy of the string in all-uppercase or all-lowercase.

When to use it: normalizing text before comparing it, or literally displaying "shouted" or "quiet" text.

"hello".toUpperCase(); // "HELLO"
public static boolean containsWord(String text, String word) {
    return text.toLowerCase().contains(word.toLowerCase());
}

.trim()

Removes plain ASCII whitespace (spaces, tabs, newlines) from the beginning and end of the string. Never touches the middle.

When to use it: cleaning up user-typed input before you use it — form fields, command-line arguments, file lines.

"  hello  ".trim(); // "hello"
public static boolean isBlank(String s) {
    return s.trim().isEmpty();
}

.strip() (Java 11+)

Does the same job as .trim(), but more correctly — it understands Unicode whitespace (like non-breaking spaces), not just the handful of ASCII characters .trim() knows about. There are also .stripLeading() and .stripTrailing() for one side only.

When to use it: the same situations as .trim(). If you're on Java 11+, prefer .strip() — it's the more robust choice.

"  hello  ".strip(); // "hello"
public static String clean(String s) {
    return s.strip();
}

.isEmpty() and .isBlank() (Java 11+)

.isEmpty() checks for a zero-length string (""). .isBlank() checks for a string that's empty or contains only whitespace (like " ") — usually the more useful check.

When to use it: validating that a user actually typed something meaningful, not just spaces.

"".isEmpty();     // true
"   ".isEmpty();  // false — it does have characters, they're just spaces
"   ".isBlank();  // true
public static boolean isBlank(String s) {
    return s.isBlank(); // Java 11+, equivalent to s.trim().isEmpty()
}

6. Splitting and Joining

.split(regex)

Breaks a string into an array of pieces, using the given text as the separator. Important detail: the separator is treated as a regular expression, not a literal string — so characters with special regex meaning (like .) need to be escaped with \\.

When to use it: breaking up delimited data — CSV fields, path segments, words in a sentence.

"a,b,c".split(","); // ["a", "b", "c"]
public static String[] parts(String version) {
    return version.split("\\."); // escaped, because "." means "any character" in regex
}

String.join(delimiter, elements)

The reverse of split() — glues a collection of strings back together with a delimiter between each pair, and no delimiter at the very start or end.

When to use it: building a delimited string from a list — a CSV row, a comma-separated summary, a file path.

java.util.List<String> words = java.util.List.of("a", "b", "c");
String.join(", ", words); // "a, b, c"
public static String joinWords(java.util.List<String> words) {
    return String.join(",", words);
}

7. Building Strings with StringBuilder

Strings are immutable, which is great for safety but can be clumsy when you're assembling a string piece by piece (say, in a loop). StringBuilder is a companion class that is mutable — you build it up in place, then convert it back to a String at the end with .toString().

new StringBuilder(s), .append(x), .toString()

append() adds text to the end of the builder and returns the same builder (so calls can be chained). .toString() converts the finished result back into a regular String.

When to use it: anywhere you're assembling a string from many pieces, especially inside a loop.

StringBuilder sb = new StringBuilder();
sb.append("Hello").append(", ").append("world!");
sb.toString(); // "Hello, world!"
public static String joinWords(java.util.List<String> words) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < words.size(); i++) {
        sb.append(words.get(i));
        if (i < words.size() - 1) {
            sb.append(",");
        }
    }
    return sb.toString();
}

.reverse()

Reverses the characters currently in the builder, in place.

When to use it: reversing text, or checking palindromes.

new StringBuilder("hello").reverse().toString(); // "olleh"
public static boolean isPalindrome(String s) {
    String reversed = new StringBuilder(s).reverse().toString();
    return s.equals(reversed);
}

.deleteCharAt(index)

Removes the character at the given index, shifting everything after it left by one.

When to use it: editing text in place — removing a single unwanted character.

new StringBuilder("hello").deleteCharAt(1).toString(); // "hllo"
public static String removeAt(String s, int index) {
    StringBuilder sb = new StringBuilder(s);
    sb.deleteCharAt(index);
    return sb.toString();
}

Remember to call .toString() at the end! StringBuilder methods return a StringBuilder, not a String — a very common bug is returning the original unreversed/unmodified String variable instead of the builder's result.


8. Formatting, Converting & Null-Safety

String.format(pattern, args...)

Builds a string from a template with placeholders — %s for any value (calls .toString() on it), %d for integers, and width/padding modifiers like %05d (pad with zeros to 5 digits).

When to use it: building any string with multiple pieces of data plugged into fixed positions — messages, reports, labels.

String.format("%s is %d years old.", "Ana", 30); // "Ana is 30 years old."
String.format("%05d", 42); // "00042"

Specifiers are matched by position — the first % matches the first argument, and so on. Putting %d where a String argument lands throws an exception at runtime.

.repeat(n) (Java 11+)

Returns the string concatenated with itself n times.

When to use it: building separators, padding, or ASCII art — anything that's the same text over and over.

"-".repeat(4); // "----"
public static String divider(int width) {
    return "=".repeat(width);
}

String.valueOf(x)

Safely converts any value — including null — to its string form. String.valueOf(null-reference) returns the literal text "null" instead of throwing, unlike calling .toString() directly on a null reference.

When to use it: converting a value you're not 100% sure is non-null, or converting a non-String type (like an int) to a String.

String.valueOf(42);       // "42"
String.valueOf((Object) null); // "null" — no exception
public static String describe(Object obj) {
    return String.valueOf(obj); // safe even if obj is null
}

What's Next

That's the toolkit. Head over to java-strings-exercises.md and put it to work — each exercise shows you a snippet using one or more of these methods and asks you to decide whether it's doing the right thing. Some are correct; some have a bug hiding in exactly the kind of detail covered above (an off-by-one substring, a forgotten reassignment, an unescaped . in a split() call). Reading the code carefully — the same way you'd review a teammate's or an AI agent's pull request — is the actual skill being built here.