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
Title
Think Java 2e · Chapter 6 · Loops and Strings
Sections 6.7-6.11 · pp. 98-103
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.
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.
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
indexOf finds where something issubstring copies part of it outequals and compareTo, never ==String.format makes formatted text without printing itDowney & 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.
Section
Section 6.7
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);
}| i | i < 6? | charAt(i) |
|---|---|---|
| 0 | yes | b |
| 1 | yes | a |
| ... | ... | ... |
| 5 | yes | a |
| 6 | no — 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.
Notation
This is the single most important line in the lesson, and the reason is entirely about where the numbering starts.
Annotate
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.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.
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| expression | index requested | valid? | result |
|---|---|---|---|
| fruit.length() | — | — | 6 |
| charAt(6) | 6 | no — indexes stop at 5 | StringIndexOutOfBoundsException |
| charAt(5) | 5 | yes | 'a' |
| charAt(length - 1) | 5 | yes | '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.
Prediction
fruit holds "banana".
int len = fruit.length();
char c = fruit.charAt(len);| len | valid indexes | index requested |
|---|---|---|
| 6 | 0 to 5 | 6 — out of range |
Predict first
What happens?
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.
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;
}| i | s.charAt(i) | r after |
|---|---|---|
| 5 | a | "a" |
| 4 | n | "an" |
| 3 | a | "ana" |
| 2 | n | "anan" |
| 1 | a | "anana" |
| 0 | b | "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".
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));
}| i | charAt(i) | outcome |
|---|---|---|
| 0 to 5 | b, a, n, a, n, a | fine |
| 6 | there is no index 6 | StringIndexOutOfBoundsException |
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.
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 want | write |
|---|---|
| visit every character forwards | for (int i = 0; i < s.length(); i++) |
| visit every character backwards | for (int i = s.length() - 1; i >= 0; i--) |
| get just the last character | s.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.
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.
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;
}| i | character appended | r |
|---|---|---|
| 2 | g | "g" |
| 1 | o | "go" |
| 0 | d | "god" |
Predict first
What does reverse("dog") return?
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.
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.
Section
Section 6.8
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| call | searches from | result | why |
|---|---|---|---|
| indexOf('a') | index 0 | 1 | the FIRST appearance |
| indexOf('a', 2) | index 2 | 3 | the next 'a' at or after 2 |
| indexOf('a', 5) | index 5 | 5 | the character is at the starting index |
| indexOf('z') | index 0 | -1 | not present |
| indexOf("nan") | index 0 | 2 | an 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.
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.
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);
}| pass | search from | returns | printed |
|---|---|---|---|
| 1 | 0 | 1 | found at 1 |
| 2 | 2 | 3 | found at 3 |
| 3 | 4 | 5 | found at 5 |
| 4 | 6 | -1 | loop 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.
Prediction
fruit is "banana".
fruit.indexOf('n', 3)| index | character | at or after 3? |
|---|---|---|
| 2 | n | no — before the start |
| 3 | a | yes, but not 'n' |
| 4 | n | yes — this is the answer |
Predict first
What is the result?
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.
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 value | meaning |
|---|---|
| 0 or more | the index where it was found |
| -1 | not 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.
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)| step | value | outcome |
|---|---|---|
| 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.
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");
}| i | the branch taken |
|---|---|
| -1 | not found |
| 0 or more | use 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.
Prediction
The character is not in the string.
"banana".indexOf('z')| outcome | value |
|---|---|
| found | the index, 0 or more |
| not found | -1 |
Predict first
What is the result?
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.
Matching
All on the string "banana".
Match the pairs
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.
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.
Section
Section 6.9
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) // ""| call | from | up to but NOT including | result |
|---|---|---|---|
| substring(0, 3) | 0 | 3 | "ban" |
| substring(2, 5) | 2 | 5 | "nan" |
| substring(6, 6) | 6 | 6 | "" — 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.
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.
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) // ""| call | equivalent two-argument form | result |
|---|---|---|
| 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.
Prediction
fruit is "banana".
fruit.substring(1, 4)| index | character | included? |
|---|---|---|
| 1 | a | yes |
| 2 | n | yes |
| 3 | a | yes |
| 4 | n | no — the end is exclusive |
Predict first
What is the result?
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.
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| variable | value after | changed? |
|---|---|---|
| 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.
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| call | characters included | result |
|---|---|---|
| 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 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 want | write |
|---|---|
| the first n characters | substring(0, n) |
| n characters starting at i | substring(i, i + n) |
| everything from i onwards | substring(i) |
| everything except the last character | substring(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.
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.
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.
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.
Section
Section 6.10
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 == compares | what you wanted compared |
|---|---|
| whether the two operands refer to the same object | whether they contain the same characters |
| two different objects, even with equal contents | equal contents |
| result: false | wanted: 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.
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.
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 value | meaning |
|---|---|
| negative | the string it was called on comes earlier in the alphabet |
| positive | it comes later |
| zero | the 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.
Prediction
The user types exactly yes.
String answer = in.nextLine();
if (answer == "yes") {
System.out.println("Let's go!");
} else {
System.out.println("Goodbye!");
}| comparison | asks | answer |
|---|---|---|
| answer == "yes" | the same object? | no |
| answer.equals("yes") | the same characters? | yes |
Predict first
What is displayed?
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.
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.
| comparison | legal? | asks | usually what you want? |
|---|---|---|---|
| 5 == 5 | yes | are these the same value? | yes |
| 'a' == 'a' | yes | the same character? | yes |
| s1 == s2 | yes | the same object? | almost never |
| s1.equals(s2) | yes | the 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.
Trap
Compiles, runs, and is wrong for every input.
String answer = in.nextLine();
if (answer == "yes") {
System.out.println("Let's go!");
}| the user types | characters equal? | same object? | condition |
|---|---|---|---|
| yes | yes | no | false |
| no | no | no | false |
| anything | sometimes | never | always 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.
Use equals, which compares the characters.
String answer = in.nextLine();
if (answer.equals("yes")) {
System.out.println("Let's go!");
}| method | compares | use for |
|---|---|---|
| equals | the characters | is this string the one I mean? |
| compareTo | alphabetical order | which of these comes first? |
| == | object identity | essentially 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.
Prediction
The sign is what matters.
"apple".compareTo("banana")| comparison | first differing characters | sign |
|---|---|---|
| "apple" vs "banana" | 'a' against 'b' | negative |
| meaning | apple comes first alphabetically | — |
Predict first
Is the result negative, zero or positive?
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.
Discrimination
== compares values for primitives and identity for objects.
Sort into buckets
Sort each comparison by whether == does what you want.
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.
Section
Section 6.11
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);
}| method | takes | does |
|---|---|---|
| System.out.printf | a format string, then values | displays the result on the screen |
| String.format | the same arguments | creates 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.
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.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.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.
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 to | use |
|---|---|
| display formatted text now | System.out.printf |
| store formatted text for later | String.format |
| return formatted text from a method | String.format |
| build text and compare or search it | String.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.
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);
}| step | value |
|---|---|
| hour < 12? | 19 < 12 is false |
| ampm | "PM" |
| hour = hour - 12 | 7 |
| %02d on 7 | 07 |
| %02d on 5 | 05 |
Predict first
What string is returned?
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.
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 character | indexOf |
| extract part of a string | substring |
| compare two strings | equals, compareTo |
| count the characters | length |
| build formatted text | String.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.
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 to | possible? |
|---|---|
| display it | yes |
| store it | no |
| compare it with another time | no |
| put it in a longer message | no |
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.
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);| use | works? |
|---|---|
| print it | yes |
| concatenate it into a longer string | yes |
| compare two of them with equals | yes |
| store a list of them | yes |
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.
Prediction
The midnight special case.
if (hour < 12) {
ampm = "AM";
if (hour == 0) {
hour = 12; // midnight
}
}| step | value |
|---|---|
| hour < 12? | 0 < 12 is true |
| ampm | "AM" |
| hour == 0? | yes — hour becomes 12 |
Predict first
What string is returned?
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.
Discrimination
One displays; the other returns.
Sort into buckets
Sort each situation.
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.
Comparison
Fill the blanks. Every one of these takes or returns an index or a string.
Comparison matrix
| method | returns | watch out for |
|---|---|---|
| length() | the number of characters | it is a method — needs parentheses |
| charAt(i) | the char at index i | valid indexes are 0 to length() - 1 |
| indexOf(c) | the index of the first occurrence, or -1 | always check for -1 before using it |
| substring(a, b) | a new string from a up to but not including b | the end index is excluded |
| equals(other) | true if the characters match | use 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.
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 fact | the consequence |
|---|---|
| indexes run 0 to length() - 1 | the forward condition is i < length() |
| the last index is length() - 1 | the backward loop starts there |
| substring's end index is excluded | the result's length is end - start |
| indexOf returns -1 when not found | check before using the result as an index |
i < s.length() — learn it as one unit.equals, never ==.String.format is printf that returns instead of printing.Check
Work it out before you click.
String s = "hello";
char c = s.charAt(s.length() - 1);| expression | value |
|---|---|
| s.length() | 5 |
| s.length() - 1 | 4 |
| s.charAt(4) | 'o' |
Check your understanding
What is c?
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.
charAt(s.length() - 2).Check
Work it out before you click.
String s = "programming";
String part = s.substring(3, 7);| index | character | included? |
|---|---|---|
| 3 | g | yes |
| 4 | r | yes |
| 5 | a | yes |
| 6 | m | yes |
| 7 | m | no |
Check your understanding
What is part?
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.
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");
}| test | asks | result |
|---|---|---|
| a == "yes" | the same object? | false |
| a.equals("yes") | the same characters? | true |
Check your understanding
What is displayed?
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.
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.
Commit first
Commit to an answer and to your confidence.
Predict first
What does "banana".substring(2, 5) return?
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.
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.
Exit ticket
One question before you close the deck.
Predict first
Why does answer == "yes" fail even when the user typed exactly yes?
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.
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.
Recap
Five sections that turn a string from something you print into something you can search, take apart and rebuild.
| if you remember one thing | it is this |
|---|---|
| about indexes | the last one is length minus one |
| about substring | the end index is excluded |
| about comparison | equals for characters, == for identity |
length() is a method and needs parentheses; indexes run from 0 to length() - 1.for (int i = 0; i < s.length(); i++) visits every character exactly once.charAt(s.length()) throws StringIndexOutOfBoundsException — subtract one for the last character.indexOf returns the first occurrence, or -1 when there is none; a second argument says where to start.substring excludes its end index, so the result's length is end minus start.substring with one argument runs to the end of the string.equals, never == — == asks whether they are the same object.compareTo gives the sign of the alphabetical difference, and both methods are case-sensitive.String.format is printf that returns the text instead of displaying it.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.