The String Toolkit: Iteration, indexOf, and substring

Iterating over a string's characters and the off-by-one that ends in an exception, indexOf for searching, substring for extracting, why == is almost never the right way to compare strings, and String.format for building formatted text you do not immediately print. Follows Think Java 2e, Chapter 6 (Loops and Strings), Sections 6.7-6.11, pp. 98-103, cross-referenced against The Java Tutorials — Strings.

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. The String Toolkit: Iteration, indexOf, and substring

Title

Think Java 2e · Chapter 6 · Loops and Strings

Sections 6.7-6.11 · pp. 98-103

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter 6 (Loops and Strings), Sections 6.7-6.11, pp. 98-103. Everything on these slides can be checked against those pages.

1. Iterate over the characters of a string with a for loop and length().

2. Explain why the last character is at index length() - 1, and name the exception you get otherwise.

3. Use indexOf to search for a character or a substring, and interpret a return value of -1.

4. Use substring with one or two arguments, and say which index is excluded.

5. Compare strings with equals and compareTo, and say why == gives the wrong answer.

6. Use String.format to build a formatted string without displaying it.

3. Retrieve before you read

Warm-up

One picture from the previous lesson decides everything in this one.

Discussion prompt

The string "banana" has how many characters? What is the index of the first one, and what is the index of the last?

Hint: Count, then count again from zero.

Answer:

Six characters. The first is at index 0 and the last is at index 5 — which is the length minus one.

Every method in this lesson takes an index, and almost every mistake in it comes from that final minus one. It is worth drawing the ruler each time until it becomes automatic.

4. Four things you do to a string

Concept

Some of the most interesting problems in computer science involve searching and manipulating text. Although the examples here are short, the techniques work the same whether you have one word or one million.

Figure (svg): The string banana with each character in a numbered box from index 0 to index 5

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 6 (Loops and Strings), Sections 6.7-6.11, pp. 98-103 — Sections 6.7-6.11, printed pages 98-103.

5. Iterating over a string

Section

Section 6.7

6. length() and charAt() together

Concept

Strings provide a method called length that returns the number of characters. Combined with charAt and a for loop, that lets you visit every character in turn.

String fruit = "banana";
for (int i = 0; i < fruit.length(); i++) {
    char letter = fruit.charAt(i);
    System.out.println(letter);
}
ii < 6?charAt(i)
0yesb
1yesa
.........
5yesa
6no — the loop ends—

Because length is a method, you have to invoke it with parentheses — there are no arguments, but the parentheses are still required. When i is equal to the length, the condition becomes false and the loop terminates.

7. Why the condition uses < and not <=

Notation

This is the single most important line in the lesson, and the reason is entirely about where the numbering starts.

Annotate

  • Indexes run from 0 to n-1, where n is the length. Six characters means indexes 0 through 5 — there is no index 6.
  • i < length() stops at exactly the right place. The last pass has i equal to 5, and the next test fails.
  • i <= length() runs one pass too many, with i equal to 6, and charAt(6) throws a StringIndexOutOfBoundsException.
  • This is the same shape as for (int i = 0; i < n; i++) from Lesson 6a, which runs exactly n times. Here n is the length, and the n passes visit the n characters.

Learn i < s.length() as a single unit. Almost every string loop you write for the rest of the book begins with it, and the version with <= is a bug every time.

8. Finding the last character

Worked example

The natural first attempt compiles, runs, and throws. Working out why is the whole point of the index ruler.

int length = fruit.length();
char last = fruit.charAt(length);        // wrong!

char last = fruit.charAt(length - 1);    // correct
expressionindex requestedvalid?result
fruit.length()——6
charAt(6)6no — indexes stop at 5StringIndexOutOfBoundsException
charAt(5)5yes'a'
charAt(length - 1)5yes'a'

Get the length.

Why: fruit.length() is 6 for "banana".

Notice that the length is not a valid index.

Why: There is no sixth letter — since we started counting at 0, the six letters are indexed 0 to 5.

Subtract one.

Why: charAt(length - 1) asks for index 5, which is the last character.

Note that this compiles either way.

Why: The mistake is a run-time error, not a compile-time one — the compiler cannot know how long the string will be.

Verify: Run both versions: the first throws StringIndexOutOfBoundsException, the second gives 'a'.

Why: Then check the exception message — it names the index that was out of range, which is usually enough to find the bug immediately.

9. What does charAt(length()) do?

Prediction

fruit holds "banana".

int len = fruit.length();
char c = fruit.charAt(len);
lenvalid indexesindex requested
60 to 56 — out of range

Predict first

What happens?

  • StringIndexOutOfBoundsException at run time
  • A compile error
  • It returns the last character, 'a'
  • It returns the empty string

Correct: StringIndexOutOfBoundsException at run time

Why: Indexes run from 0 to length minus one, so for a six-character string index 6 does not exist. This compiles cleanly — the compiler cannot know how long the string will be at run time — and throws when the line executes, which makes it a run-time error rather than a compile-time one.

10. Reading one string and building another

Concept

Many string algorithms involve reading one string and building another. To reverse a string, concatenate one character at a time — iterating the indexes in reverse order.

public static String reverse(String s) {
    String r = "";
    for (int i = s.length() - 1; i >= 0; i--) {
        r += s.charAt(i);
    }
    return r;
}
is.charAt(i)r after
5a"a"
4n"an"
3a"ana"
2n"anan"
1a"anana"
0b"ananab"

empty string — The string "", which contains no characters and has a length of zero.

The initial value of r is "", the empty string. Each pass the += operator appends the next character, and when the loop exits r contains the characters of s in reverse order — so reverse("banana") is "ananab".

11. Looping to length instead of length - 1

Trap

The trap

Off by one at the top. The loop runs one pass too many.

for (int i = 0; i <= fruit.length(); i++) {
    System.out.println(fruit.charAt(i));
}
icharAt(i)outcome
0 to 5b, a, n, a, n, afine
6there is no index 6StringIndexOutOfBoundsException

It prints the whole word correctly and then crashes, which is a particularly confusing symptom: the output looks right, and the exception appears after it.

The fix

i < length() — the loop stops exactly one place before the length.

for (int i = 0; i < fruit.length(); i++) {
    System.out.println(fruit.charAt(i));
}
what you wantwrite
visit every character forwardsfor (int i = 0; i < s.length(); i++)
visit every character backwardsfor (int i = s.length() - 1; i >= 0; i--)
get just the last characters.charAt(s.length() - 1)

Notice the backwards version: it starts at length() - 1 and its condition is i >= 0, because 0 is a valid index and must be included. Both loops visit exactly n characters.

12. Iterate over every character

Fill the middle

Print each character of s on its own line.

Fill in the blanks

for (int i = 0; i < s.length(); i++) ___

Why: < stops the loop when i reaches the length, so the last pass uses index length minus one — the last valid index. length is a method so it needs parentheses even though it takes no arguments, which is a common slip for anyone used to a length field.

13. What does reverse return?

Prediction

Trace the loop backwards.

public static String reverse(String s) {
    String r = "";
    for (int i = s.length() - 1; i >= 0; i--) {
        r += s.charAt(i);
    }
    return r;
}
icharacter appendedr
2g"g"
1o"go"
0d"god"

Predict first

What does reverse("dog") return?

  • "god"
  • "dog"
  • "ogd"
  • the empty string

Correct: "god"

Why: The loop starts at the last index and works down to 0, appending each character to r in turn, so the characters come out in the opposite order. Starting at s.length() - 1 and testing i >= 0 visits every index exactly once, including index 0.

14. Why does length need parentheses?

Socratic

It takes no arguments, so the parentheses look redundant.

Discussion prompt

s.length() has empty parentheses. Why does Java require them, given there is nothing to pass? What would be ambiguous without them?

Hint: What else could s.length mean?

Answer:

The parentheses are how Java distinguishes calling a method from naming a variable. s.length would mean a field called length; s.length() means call the method called length.

That distinction is the same one from Lesson 3a — Math.PI is a constant and has no parentheses, while Math.sqrt(x) is a method and does. It becomes concrete in Chapter 7, where arrays really do have a length field with no parentheses, which is a well-known source of confusion precisely because strings do it the other way.

15. The indexOf method

Section

Section 6.8

16. Search for a character and get its index

Concept

To search for a specific character you could write a for loop using charAt. But the String class already provides a method for exactly that.

String fruit = "banana";
int index = fruit.indexOf('a');        // returns 1
int index2 = fruit.indexOf('a', 2);    // returns 3
int missing = fruit.indexOf('z');      // returns -1
int word = fruit.indexOf("nan");       // returns 2
callsearches fromresultwhy
indexOf('a')index 01the FIRST appearance
indexOf('a', 2)index 23the next 'a' at or after 2
indexOf('a', 5)index 55the character is at the starting index
indexOf('z')index 0-1not present
indexOf("nan")index 02an entire string, not just a character

The letter 'a' appears three times, so it is not obvious what indexOf should do. According to the documentation, it returns the index of the first appearance.

17. Searching from a starting index

Picture it

A second argument says where in the string to start looking. Drawing it makes the answer obvious.

Figure (svg): The string banana with index 2 marked as the search start and index 3 marked as the result

If the character happens to appear at the starting index, that index is the answer — so fruit.indexOf('a', 5) returns 5 rather than skipping past it.

18. Finding every occurrence

Worked example

The two-argument form exists so you can keep searching. Use it in a loop to find all three 'a's.

String fruit = "banana";
int index = fruit.indexOf('a');
while (index != -1) {
    System.out.println("found at " + index);
    index = fruit.indexOf('a', index + 1);
}
passsearch fromreturnsprinted
101found at 1
223found at 3
345found at 5
46-1loop ends

Find the first occurrence with the one-argument form.

Why: indexOf('a') gives 1.

Loop while the result is not -1.

Why: This is Lesson 6a's indefinite loop — you do not know how many occurrences there are, so a while loop is right.

Search again starting one past the last hit.

Why: index + 1, because starting at index would find the same character again and loop forever.

Stop when indexOf returns -1.

Why: Since indexes cannot be negative, -1 is a safe value to mean 'not found'.

Verify: Expect three lines: found at 1, 3 and 5.

Why: Then remove the + 1 and predict what happens: the search restarts at the same index every time, finds the same character, and the loop never ends. That single character is the difference between a search and an infinite loop.

19. What does this return?

Prediction

fruit is "banana".

fruit.indexOf('n', 3)
indexcharacterat or after 3?
2nno — before the start
3ayes, but not 'n'
4nyes — this is the answer

Predict first

What is the result?

  • 4
  • 2
  • 3
  • -1

Correct: 4

Why: The search starts at index 3 and moves right, so the 'n' at index 2 is skipped and the one at index 4 is found. The two-argument form searches at or after the given index, which is what makes it usable for finding successive occurrences.

20. Why -1 means not found

Concept

A method that returns an index has to be able to say there is no index. Returning -1 is a convention, and it works because of a fact about indexes.

return valuemeaning
0 or morethe index where it was found
-1not present anywhere in the string
(never)any other negative number

Since indexes cannot be negative, this value indicates the character was not found. The convention depends on that: -1 can never be confused with a real answer. It also means you must always check for it — using an unchecked indexOf result as an index is how a search failure turns into an exception.

21. Using an indexOf result without checking it

Trap

The trap

The search fails and the result is used anyway.

String fruit = "banana";
int i = fruit.indexOf('z');      // -1
char c = fruit.charAt(i);        // charAt(-1)
stepvalueoutcome
indexOf('z')-1'z' is not in the string
charAt(-1)—StringIndexOutOfBoundsException

The exception names index -1, which is a strong clue: a negative index almost always means an unchecked indexOf result.

The fix

Check for -1 before using the result.

int i = fruit.indexOf('z');
if (i != -1) {
    char c = fruit.charAt(i);
    System.out.println("found " + c + " at " + i);
} else {
    System.out.println("not found");
}
ithe branch taken
-1not found
0 or moreuse it as an index

This is Lesson 5b's ask before you act in another form: a method that may fail returns a value saying so, and it is your job to look at it. Ignoring a return value that reports failure is one of the most common sources of run-time errors in any language.

22. What does indexOf return when it fails?

Prediction

The character is not in the string.

"banana".indexOf('z')
outcomevalue
foundthe index, 0 or more
not found-1

Predict first

What is the result?

  • -1
  • 0
  • 6
  • an exception

Correct: -1

Why: indexOf returns -1 to indicate the character was not found. The convention works because indexes can never be negative, so -1 cannot be mistaken for a real position — but it does mean you must check the result before using it as an index.

23. Match each call to its result

Matching

All on the string "banana".

Match the pairs

  • a. indexOf('b')
  • b. indexOf('a')
  • c. indexOf('a', 5)
  • d. indexOf("nan")
  • r1. 0
  • r2. 1
  • r3. 5
  • r4. 2

Why: The 'b' is first at index 0 and the first 'a' is at 1. Starting the search at 5 finds the 'a' that is already there, because if the character appears at the starting index, that index is the answer. And indexOf can search for a whole string: "nan" begins at index 2.

24. What would happen without the + 1?

Edge cases

The find-all loop searches from index + 1. Remove it.

Discussion prompt

In the loop that finds every 'a', the next search starts at index + 1. What happens if it starts at index instead — and what general rule does that suggest about search loops?

Hint: What does indexOf return if the character is at the starting index?

Answer:

It finds the same character again, every time, and the loop never ends. indexOf('a', 1) returns 1, so index never changes and the condition index != -1 stays true forever.

The general rule: a search loop must make progress past what it just found. The + 1 is the update from Lesson 6a — the statement that moves the variable toward the exit condition — and without it this is exactly the 'body changes nothing' infinite loop.

25. Substrings

Section

Section 6.9

26. Copy part of a string out

Concept

In addition to searching strings, we often need to extract parts of them. The substring method returns a new string copying letters from an existing one, given a pair of indexes.

String fruit = "banana";
fruit.substring(0, 3)    // "ban"
fruit.substring(2, 5)    // "nan"
fruit.substring(6, 6)    // ""
callfromup to but NOT includingresult
substring(0, 3)03"ban"
substring(2, 5)25"nan"
substring(6, 6)66"" — nothing between them

The character indicated by the second index is not included. That looks arbitrary and is not: defining substring this way simplifies some common operations.

27. Where the two indexes point

Picture it

Think of the indexes as pointing at the gaps between characters rather than at the characters themselves. Then the exclusive end stops being strange.

Figure (svg): The string banana with indexes 2 and 5 marked, showing that substring 2 to 5 returns the characters at 2, 3 and 4

The useful consequence: the length of the result is second index minus first. So to select a substring of length len starting at index i, you write fruit.substring(i, i + len) — which is why excluding the end is convenient rather than awkward.

28. The one-argument form

Worked example

Like most string methods, substring is overloaded — there are other versions with different parameters. Invoked with one argument it returns everything from that index to the end.

fruit.substring(0)    // "banana"
fruit.substring(2)    // "nana"
fruit.substring(6)    // ""
callequivalent two-argument formresult
substring(0)substring(0, 6)"banana" — a copy of the whole string
substring(2)substring(2, 6)"nana" — all but the first two characters
substring(6)substring(6, 6)"" — the argument is the length

Note that one argument means 'to the end'.

Why: fruit.substring(2) is the same as fruit.substring(2, fruit.length()).

Prefer the one-argument form when you want the end.

Why: It is more convenient, and it cannot get the end index wrong.

Note the empty result.

Why: substring returns the empty string if the argument is the length of the string — which is a valid answer rather than an error.

Verify: Check that substring(2) and substring(2, fruit.length()) give the same result.

Why: They do. The one-argument version exists purely for convenience, and that convenience is real: it removes the one place where you could write length() and mean length() - 1.

29. What does this return?

Prediction

fruit is "banana".

fruit.substring(1, 4)
indexcharacterincluded?
1ayes
2nyes
3ayes
4nno — the end is exclusive

Predict first

What is the result?

  • "ana"
  • "anan"
  • "nan"
  • "ban"

Correct: "ana"

Why: The characters at indexes 1, 2 and 3 are included and the one at index 4 is not, giving three characters — which is 4 minus 1. The length of the result is always the second index minus the first.

30. substring never changes the original

Concept

It returns a new string that copies letters from the existing one. The original is untouched — which is a property of every String method you will meet.

String fruit = "banana";
String part = fruit.substring(0, 3);
System.out.println(part);    // ban
System.out.println(fruit);   // banana - unchanged
variablevalue afterchanged?
fruit"banana"no
part"ban"newly created

This matters more than it looks. fruit.substring(0, 3); on a line by itself does nothing useful — the new string is created and immediately discarded, exactly like the ignored return value in Lesson 4b. Chapter 9 gives this behaviour its proper name: Strings are immutable.

31. Expecting the end index to be included

Trap

The trap

Off by one at the end. The result is one character shorter than expected.

String fruit = "banana";
// wanting the first three letters, "ban":
String first = fruit.substring(0, 2);   // "ba" - too short
callcharacters includedresult
substring(0, 2)indexes 0 and 1"ba"
substring(0, 3)indexes 0, 1 and 2"ban"

The instinct is to name the last character you want. substring wants you to name the first character you do not want.

The fix

The second index is exclusive, so the length is the difference between the two.

fruit.substring(0, 3)        // "ban" - 3 characters
fruit.substring(2, 5)        // "nan" - 3 characters
fruit.substring(i, i + len)  // len characters, starting at i
you wantwrite
the first n characterssubstring(0, n)
n characters starting at isubstring(i, i + n)
everything from i onwardssubstring(i)
everything except the last charactersubstring(0, s.length() - 1)

The second row is the one to remember. Because the end is exclusive, the arithmetic is simply i + len — no minus one anywhere. That is the simplification the exclusive end buys you.

32. Take the first three characters

Fill the middle

Get "ban" from "banana".

Fill in the blanks

String first = fruit.substring(0, 3);

Why: Starting at 0 and ending before 3 includes indexes 0, 1 and 2 — three characters. Because the end index is exclusive, the second argument is the same as the number of characters you want when you start at zero, which is one of the simplifications the convention buys.

33. How many characters does each return?

Definition probe

The length is always the second index minus the first.

Sort into buckets

Sort each call on "banana" by the length of its result.

0 characters
substring(6, 6)
1 character
substring(4, 5)
3 characters
substring(0, 3); substring(2, 5)
4 characters
substring(2)
zero
The two indexes are equal, so there is nothing between them — substring returns the empty string, which is a valid result rather than an error.
one
The indexes differ by one, so exactly one character lies between them.
three
The indexes differ by three.
four
The one-argument form runs to the end, so from index 2 of a six-character string that is four characters: "nana".

34. Why exclude the end index?

Explain it to yourself

It looks like a design mistake until you use it.

Discussion prompt

substring(2, 5) returns three characters rather than four. Explain what this convention makes easy — and check your explanation against the expression for 'len characters starting at i'.

Hint: Write down the two-argument call for a substring of a given length.

Answer:

It makes the length arithmetic clean: the result's length is simply end - start, and a substring of length len starting at i is substring(i, i + len) with no adjustment anywhere.

It also makes adjacent substrings join neatly: substring(0, 3) and substring(3, 6) together cover the whole string with no gap and no overlap, because the end of one is the start of the next.

The same convention appears in the for (int i = 0; i < n; i++) loop, which visits n items, and in almost every language's slicing operation. Once you notice it is the same idea, both stop being arbitrary.

35. Comparing strings

Section

Section 6.10

36. == almost never works on strings

Concept

When comparing strings it is tempting to use == and !=. But that will almost never work. The following code compiles and runs, and always displays Goodbye! regardless of what the user types.

System.out.print("Play again? ");
String answer = in.nextLine();
if (answer == "yes") {          // wrong!
    System.out.println("Let's go!");
} else {
    System.out.println("Goodbye!");
}
what == compareswhat you wanted compared
whether the two operands refer to the same objectwhether they contain the same characters
two different objects, even with equal contentsequal contents
result: falsewanted: true

The problem is that == checks whether the two operands refer to the same object. Even if the answer is "yes", it refers to a different object in memory than the literal "yes" in your code. Chapter 7 explains objects and references properly.

37. Same characters, different objects

Picture it

Two strings can hold identical characters and still be two separate things in memory. == asks about the things; equals asks about the characters.

Figure (svg): Two panels contrasting what the equality operator compares with what the equals method compares

The correct way to compare strings is with the equals method: answer.equals("yes"). It returns true if the strings contain the same characters, and false otherwise.

38. compareTo, and what its number means

Worked example

If two strings differ, compareTo says which comes first in alphabetical order — and the number it returns is more informative than a simple yes or no.

String name1 = "Alan Turing";
String name2 = "Ada Lovelace";
int diff = name1.compareTo(name2);
if (diff < 0) {
    System.out.println("name1 comes before name2.");
} else if (diff > 0) {
    System.out.println("name2 comes before name1.");
} else {
    System.out.println("The names are the same.");
}
return valuemeaning
negativethe string it was called on comes earlier in the alphabet
positiveit comes later
zerothe strings are equal
8, in this example"l" comes 8 letters after "d"

Call compareTo on one string, passing the other.

Why: The direction matters: the result describes the string the method was called on.

Compare the result against zero, not against a specific number.

Why: diff < 0, diff > 0, diff == 0 — the sign is what carries the meaning.

Understand where the number comes from.

Why: It is the difference between the first characters that are not the same. 'Ada' and 'Alan' agree on 'A', then differ: 'l' minus 'd' is 8.

Note that both methods are case-sensitive.

Why: In Unicode uppercase letters come before lowercase, so "Ada" comes before "ada".

Verify: Expect name2 comes before name1. — Ada Lovelace sorts before Alan Turing.

Why: Then swap the two variables and confirm the sign flips. The magnitude is rarely useful; the sign is the whole answer, which is why the code tests against zero rather than against 8.

39. What does this print?

Prediction

The user types exactly yes.

String answer = in.nextLine();
if (answer == "yes") {
    System.out.println("Let's go!");
} else {
    System.out.println("Goodbye!");
}
comparisonasksanswer
answer == "yes"the same object?no
answer.equals("yes")the same characters?yes

Predict first

What is displayed?

  • Goodbye!
  • Let's go!
  • nothing
  • a compile error

Correct: Goodbye!

Why: == compares whether the two operands refer to the same object, and a string typed at the keyboard is a different object from the literal in your source, so the condition is false and the else branch runs. The code compiles and runs — this is a logic error, and it always displays Goodbye! no matter what is typed.

40. Why == compiles at all

Concept

If == is almost never right for strings, it would be helpful if the compiler rejected it. It cannot, and the reason is worth knowing.

comparisonlegal?asksusually what you want?
5 == 5yesare these the same value?yes
'a' == 'a'yesthe same character?yes
s1 == s2yesthe same object?almost never
s1.equals(s2)yesthe same characters?yes

For primitive types — int, char, double, boolean — == compares values and is exactly right. For objects it compares identity, and a String is an object. The comparison is legal in both cases, which is why the compiler cannot help and why the rule has to be learned.

41. Comparing user input with ==

Trap

The trap

Compiles, runs, and is wrong for every input.

String answer = in.nextLine();
if (answer == "yes") {
    System.out.println("Let's go!");
}
the user typescharacters equal?same object?condition
yesyesnofalse
nononofalse
anythingsometimesneveralways false

The third row is the giveaway. A condition that is false for every possible input is not a comparison at all — and no error message appears, because comparing two objects with == is perfectly legal Java.

The fix

Use equals, which compares the characters.

String answer = in.nextLine();
if (answer.equals("yes")) {
    System.out.println("Let's go!");
}
methodcomparesuse for
equalsthe charactersis this string the one I mean?
compareToalphabetical orderwhich of these comes first?
==object identityessentially never, for strings

A useful habit: when you find yourself writing == and either operand is a String, stop and write .equals( instead. There is a case where == on strings is meaningful, and you will not meet it in this book.

42. What does compareTo return?

Prediction

The sign is what matters.

"apple".compareTo("banana")
comparisonfirst differing characterssign
"apple" vs "banana"'a' against 'b'negative
meaningapple comes first alphabetically—

Predict first

Is the result negative, zero or positive?

  • Negative — apple comes before banana
  • Positive — banana is longer
  • Zero — they are both fruit
  • Positive — apple comes before banana

Correct: Negative — apple comes before banana

Why: compareTo returns a negative number when the string it is called on comes earlier in the alphabet. The first characters differ — 'a' against 'b' — so the comparison is decided there and the result is 'a' minus 'b', which is -1. Length plays no part unless one string is a prefix of the other.

43. == or equals?

Discrimination

== compares values for primitives and identity for objects.

Sort into buckets

Sort each comparison by whether == does what you want.

== is right
two ints; two chars; two doubles
use equals instead
two Strings; a String and a literal
eq
These are primitive types, and for primitives == compares the actual values, which is what you want. (Doubles are a special case for a different reason — see Lesson 2b on rounding error.)
meth
A String is an object, so == asks whether the two references point at the same object rather than whether the characters match. Use equals, which compares contents.

44. Is == on strings ever true?

Counterexample

'Almost never work' is a careful phrase. Find the exception.

Discussion prompt

Think Java says == on strings will almost never work, not never. Can you construct a case where it is true — and does that make it safe to rely on?

Hint: What if both strings are literals in your source code?

Answer:

String a = "yes"; String b = "yes"; a == b is true, because Java stores identical string literals as a single shared object. Two literals with the same characters really are the same object.

But that is exactly what makes it dangerous. The comparison works in a small test where both strings are literals, and fails the moment one of them comes from user input, a file, or concatenation — which is when it matters.

A rule that works only in the cases you test is worse than no rule at all. Use equals unconditionally, and the question never arises.

45. String formatting

Section

Section 6.11

46. Build a formatted string without printing it

Concept

Section 3.5 showed how to display formatted output with printf. Sometimes a program needs to create strings formatted a certain way without displaying them immediately, or ever.

public static String timeString(int hour, int minute) {
    String ampm;
    if (hour < 12) {
        ampm = "AM";
        if (hour == 0) {
            hour = 12;              // midnight
        }
    } else {
        ampm = "PM";
        hour = hour - 12;
    }
    return String.format("%02d:%02d %s", hour, minute, ampm);
}
methodtakesdoes
System.out.printfa format string, then valuesdisplays the result on the screen
String.formatthe same argumentscreates a new string and displays nothing

String.format takes exactly the same arguments as System.out.printf — a format string followed by a sequence of values. The only difference is what happens to the result.

47. Reading timeString

Notation

This method uses almost everything from the last three chapters. Worth going through slowly.

Annotate

  • String ampm; is declared before the if. This is the case Lesson 2a's probe asked about: the value depends on which branch runs, so it cannot be initialised on the declaration line.
  • The nested if handles midnight. Hour 0 in 24-hour time is 12 AM, not 0 AM — a special case that would be a bug if left out.
  • The parameter hour is modified. That is safe: it is a local variable in this method, and changing it has no effect on the caller (Lesson 4a).
  • %02d means a two-digit integer padded with zeros, so 7 becomes 07 and the times line up.
  • %s takes the String. Three specifiers, three values, matched in order — exactly as in Lesson 3a.
  • So timeString(19, 5) returns "07:05 PM" — and returns it, rather than printing it, which is what lets the caller decide what to do with it.

Notice how much of the book is in this one method: a conditional chain, a nested if, a modified parameter, a return value, and a format string.

48. printf or String.format

Worked example

The two do the same formatting. Choosing between them is a decision about who decides what happens to the text.

// prints immediately, returns nothing
System.out.printf("%02d:%02d %s%n", hour, minute, ampm);

// returns the text, prints nothing
String t = String.format("%02d:%02d %s", hour, minute, ampm);
System.out.println(t);          // the CALLER decides
you want touse
display formatted text nowSystem.out.printf
store formatted text for laterString.format
return formatted text from a methodString.format
build text and compare or search itString.format

Ask whether the text is going straight to the screen.

Why: If so, printf is simpler — one call, no variable.

Ask whether anything else might want it.

Why: If a method should hand text back, it has to return a String, so String.format is the only option.

Note the parallel with Lesson 4b.

Why: A method that formats and prints can only ever print. A method that formats and returns can be used anywhere.

Verify: Check that timeString(19, 5) returns "07:05 PM" and that nothing is displayed until you print it.

Why: Then use the returned string for something other than printing — comparing it with another time, say. That is impossible with the printf version, which is the whole argument for String.format.

49. What does timeString(19, 5) return?

Prediction

Trace the branches.

public static String timeString(int hour, int minute) {
    String ampm;
    if (hour < 12) {
        ampm = "AM";
        if (hour == 0) {
            hour = 12;              // midnight
        }
    } else {
        ampm = "PM";
        hour = hour - 12;
    }
    return String.format("%02d:%02d %s", hour, minute, ampm);
}
stepvalue
hour < 12?19 < 12 is false
ampm"PM"
hour = hour - 127
%02d on 707
%02d on 505

Predict first

What string is returned?

  • "07:05 PM"
  • "7:5 PM"
  • "19:05 PM"
  • "07:05 AM"

Correct: "07:05 PM"

Why: 19 is not less than 12, so ampm becomes PM and hour is reduced by 12 to 7. The %02d specifier pads each number to two digits with a leading zero, giving 07 and 05, and %s inserts the PM. The padding is what makes a column of times line up.

50. Read the documentation before reinventing

Concept

Think Java closes the chapter with advice worth taking literally: be sure to skim through the documentation for String. Knowing what other methods are there will help you avoid reinventing the wheel.

you might write a loop to...there is already a method
find a characterindexOf
extract part of a stringsubstring
compare two stringsequals, compareTo
count the characterslength
build formatted textString.format

Every one of these could be written by hand with charAt and a loop — and the first section of this lesson did exactly that for searching, before revealing indexOf. Writing it once yourself is how you understand it; using the library version is how you write correct code afterwards.

51. Using printf where a String was needed

Trap

The trap

A method that formats and prints cannot return the text.

public static void timeString(int hour, int minute) {
    System.out.printf("%02d:%02d%n", hour, minute);
}

// the caller cannot do anything else with it:
// String t = timeString(19, 5);     <- there is nothing to assign
the caller wants topossible?
display ityes
store itno
compare it with another timeno
put it in a longer messageno

This is Lesson 4b's point in a new setting: a method that displays its answer can only ever display it. Returning the value leaves the decision to the caller.

The fix

Format and return; let the caller print.

public static String timeString(int hour, int minute) {
    ...
    return String.format("%02d:%02d %s", hour, minute, ampm);
}

// now the caller has choices:
System.out.println(timeString(19, 5));
String header = "Departs at " + timeString(19, 5);
useworks?
print ityes
concatenate it into a longer stringyes
compare two of them with equalsyes
store a list of themyes

Compute and return; display somewhere else. It is the same rule as Lesson 4b's, and it is what makes a method reusable rather than single-purpose.

52. What does timeString(0, 30) return?

Prediction

The midnight special case.

if (hour < 12) {
    ampm = "AM";
    if (hour == 0) {
        hour = 12;              // midnight
    }
}
stepvalue
hour < 12?0 < 12 is true
ampm"AM"
hour == 0?yes — hour becomes 12

Predict first

What string is returned?

  • "12:30 AM"
  • "00:30 AM"
  • "12:30 PM"
  • "0:30 AM"

Correct: "12:30 AM"

Why: Hour 0 in 24-hour time is midnight, which is written as 12 AM rather than 0 AM in 12-hour time, so the nested if replaces the 0 with 12. Without that special case the method would return "00:30 AM", which is not how anyone writes the time — a small, easily forgotten edge case of exactly the kind worth testing deliberately.

53. printf or String.format?

Discrimination

One displays; the other returns.

Sort into buckets

Sort each situation.

System.out.printf
show a formatted total to the user right now; print a table row inside a loop
String.format
a method that returns a formatted time; build a line of text to compare with another
pf
The text is going straight to the screen and nothing else needs it, so printing directly is simplest.
sf
Something other than the screen needs the text — a return value, a comparison, a longer string — and only a method that RETURNS the text makes that possible.

54. Which of these have you now written by hand?

Real world

This lesson wrote a search loop and then revealed indexOf, and wrote a reverse method the library does not have.

Discussion prompt

You have written loops that do what indexOf does. Was that wasted effort? And how would you decide, in future, whether to look for a library method or write it yourself?

Hint: The two questions have different answers.

Answer:

Not wasted: writing the search by hand is how you understand what indexOf is doing, what it costs, and why it returns -1. That understanding is what lets you use it correctly.

But for writing working code, look for the library method first. It is tested, it handles edge cases you have not thought of, and it says what it means. Think Java's phrasing is exact: knowing what other methods are there will help you avoid reinventing the wheel.

The decision rule: if the task has an obvious name — search, extract, compare, format — someone has almost certainly written it. A web search for Java and the class name is the fastest way to find out, and it is worth the thirty seconds every time.

55. The String methods from this lesson

Comparison

Fill the blanks. Every one of these takes or returns an index or a string.

Comparison matrix

methodreturnswatch out for
length()the number of charactersit is a method — needs parentheses
charAt(i)the char at index ivalid indexes are 0 to length() - 1
indexOf(c)the index of the first occurrence, or -1always check for -1 before using it
substring(a, b)a new string from a up to but not including bthe end index is excluded
equals(other)true if the characters matchuse this rather than ==

Four of the five involve an index, and three of the five have an off-by-one waiting in them. Drawing the ruler is not a beginner's crutch — it is the fastest way to get these right.

56. The pattern to carry away

Pattern

Nearly every string operation is one of three loops or one library call. Learn the loops, then use the calls.

// visit every character
for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
}

// visit them backwards
for (int i = s.length() - 1; i >= 0; i--) {
    char c = s.charAt(i);
}

// build a new string as you go
String result = "";
for (int i = 0; i < s.length(); i++) {
    result += s.charAt(i);
}
the index factthe consequence
indexes run 0 to length() - 1the forward condition is i < length()
the last index is length() - 1the backward loop starts there
substring's end index is excludedthe result's length is end - start
indexOf returns -1 when not foundcheck before using the result as an index

57. Check: the last character

Check

Work it out before you click.

String s = "hello";
char c = s.charAt(s.length() - 1);
expressionvalue
s.length()5
s.length() - 14
s.charAt(4)'o'

Check your understanding

What is c?

  • A. 'o' (correct)
  • B. 'l'
  • C. 'h'
  • D. an exception is thrown

Answer: A

Why: "hello" has five characters at indexes 0 to 4, so length minus one is 4 and charAt(4) is the final 'o'. Using s.charAt(s.length()) instead would ask for index 5, which does not exist, and would throw StringIndexOutOfBoundsException.

Why B tempts people
This is the character at index 3, which would be charAt(s.length() - 2).
Why C tempts people
This is the first character, at index 0.
Why D tempts people
Subtracting one keeps the index in range; it is omitting the subtraction that throws.

58. Check: substring

Check

Work it out before you click.

String s = "programming";
String part = s.substring(3, 7);
indexcharacterincluded?
3gyes
4ryes
5ayes
6myes
7mno

Check your understanding

What is part?

  • A. "gram" (correct)
  • B. "gramm"
  • C. "ogra"
  • D. "ram"

Answer: A

Why: substring includes the character at the first index and excludes the one at the second, so indexes 3, 4, 5 and 6 are taken — four characters, which is 7 minus 3. The result's length is always the difference between the two arguments.

Why B tempts people
This includes the character at index 7, but the end index is exclusive.
Why C tempts people
This starts at index 2 rather than 3 — the first argument is included, so counting must start there.
Why D tempts people
This starts at index 4; index 3 is included in the result.

59. Check: comparing strings

Check

Work it out before you click.

String a = in.nextLine();     // the user types: yes
if (a == "yes") {
    System.out.println("A");
} else if (a.equals("yes")) {
    System.out.println("B");
} else {
    System.out.println("C");
}
testasksresult
a == "yes"the same object?false
a.equals("yes")the same characters?true

Check your understanding

What is displayed?

  • A. B (correct)
  • B. A
  • C. C
  • D. A then B

Answer: A

Why: The == test asks whether the two operands are the same object, and a string read from the keyboard is a different object from the literal in the source, so it is false. The equals test compares the characters and is true, so B is printed.

Why B tempts people
This would require the two to be the same object in memory, which typed input never is.
Why C tempts people
The equals test succeeds, so the chain never reaches the final else.
Why D tempts people
This is a chain, so at most one branch runs — and the first condition is false in any case.

60. Off-by-one, the most famous bug class there is

Real world

This lesson has three separate off-by-one hazards in it: the loop condition, the last index, and substring's exclusive end.

Discussion prompt

Why do you think off-by-one errors are so common — and what do the three cases in this lesson have in common that makes them all avoidable by the same habit?

Hint: Where does the counting start?

Answer:

They are common because counting from zero conflicts with how people count. A string of six characters has a last index of five, and every instinct says six.

All three cases here are the same fact seen from different angles: indexes run from 0 to n-1. The loop condition, the last index, and substring's exclusive end are all consequences of it, and drawing the ruler resolves all three at once.

The habit: when an index is involved and you are not certain, draw the boxes and number them. It takes ten seconds, it is not a beginner's crutch, and experienced programmers do it too — which is why the book draws Figure 6.3 at exactly this point.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

What does "banana".substring(2, 5) return?

  • "nan"
  • "nana"
  • "ana"
  • "nan" plus an exception

Correct: "nan"

Why: The characters at indexes 2, 3 and 4 are included and the one at index 5 is excluded, giving three characters — which is 5 minus 2. The exclusive end is what makes the length arithmetic clean: a substring of length len starting at i is always substring(i, i + len), with no adjustment. If you answered "nana", you included index 5, which is the single most common substring mistake.

62. Explain it to someone else

Explain it

Two minutes, drawing as you go.

Discussion prompt

A classmate's program prints a whole word correctly and then crashes with StringIndexOutOfBoundsException. Draw the index ruler for a five-letter word and use it to explain both why the output looked right and where the crash came from.

Hint: The crash happens after the last correct character, not instead of it.

Answer:

Draw five boxes numbered 0 to 4. Their loop condition is i <= s.length(), so i goes 0, 1, 2, 3, 4 — all fine, and all five letters print — and then 5. There is no box numbered 5, so charAt(5) throws.

That is why the output looks right: every valid character really was printed. The crash is one pass later, on an index that does not exist.

The fix is i < s.length(). And the general lesson: when a program produces correct output and then fails, suspect a loop that runs one pass too many.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

Why does answer == "yes" fail even when the user typed exactly yes?

  • Because == compares whether the two are the same object, not whether they hold the same characters
  • Because nextLine adds an invisible newline to the string
  • Because == cannot be used with String at all — it is a compile error
  • Because string literals are case-sensitive

Correct: Because == compares whether the two are the same object, not whether they hold the same characters

Why: A String is an object, and for objects == asks about identity — are these two references pointing at the same thing in memory? A string typed at the keyboard is a different object from the literal in your source, even when the characters match, so the comparison is false. It compiles perfectly well, which is why this is a logic error rather than a compile error, and the fix is answer.equals("yes"), which compares contents.

64. Draw the whole lesson

Connect it up

One page, from memory.

Draw it

Draw the index ruler for the word banana, numbering every box. Use it to answer, in writing: what is length(), what is the last valid index, what does charAt(5) give, what does indexOf('n', 3) give, and what does substring(2, 5) give. Then write the forward and backward iteration loops from memory, and one sentence saying why == is the wrong way to compare two strings.

65. Recap

Recap

Five sections that turn a string from something you print into something you can search, take apart and rebuild.

if you remember one thingit is this
about indexesthe last one is length minus one
about substringthe end index is excluded
about comparisonequals for characters, == for identity

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 6 (Loops and Strings), Sections 6.7-6.11, pp. 98-103
  2. The Java Tutorials — Strings
  3. Think Java 2e — free online edition and source code

Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108