Pseudorandom numbers and why a computer cannot produce real ones, counting values into a histogram, the single-pass trick of using a value as an index, the enhanced for loop, and the doubloon program that pulls the whole chapter together. Follows Think Java 2e, Chapter 7 (Arrays and References), Sections 7.6-7.9, pp. 115-121, cross-referenced against Java SE 21 API — java.util.Random.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 7 · Arrays and References
Sections 7.6-7.9 · pp. 115-121
Objectives
This lesson follows Think Java 2e, Chapter 7 (Arrays and References), Sections 7.6-7.9, pp. 115-121. Everything on these slides can be checked against those pages.
1. Use java.util.Random to fill an array with pseudorandom values, and say what nextInt(n) returns.
2. Explain what deterministic means and why pseudorandom is not the same as random.
3. Write a counting reduce that counts elements falling in a range.
4. Build a histogram in a single pass by using a value as an index into a counter array.
5. Write an enhanced for loop, and say when it cannot be used.
6. Convert a character to an array index with letter - 'a', and explain why that works.
Warm-up
One fact from the previous lesson decides how the histogram is built.
Discussion prompt
What are the elements of new int[100] immediately after it is created? And what are the legal indexes for that array?
Hint: The new operator does something for you.
Answer:
All 100 zeros, because new initialises every element. The legal indexes are 0 to 99.
Both facts matter in this lesson. An array of counters is ready to count the moment it is created — no initialisation loop needed — and the range 0 to 99 happens to be exactly the range of scores the histogram counts. That coincidence is the trick the chapter is building toward.
Concept
The chapter opened with a problem: counting 26 letters without declaring 26 variables. This lesson finishes it. The key move is to stop thinking of an index as a position and start thinking of it as a name for a counter.
Figure (svg): An array of counters with the element at index 62 highlighted, showing a score of 62 incrementing its own counter
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 7 (Arrays and References), Sections 7.6-7.9, pp. 115-121 — Sections 7.6-7.9, printed pages 115-121.
Section
Section 7.6
Concept
Most programs do the same thing every time they run; such programs are deterministic. Usually that is a good thing — we expect the same calculation to yield the same result. But games, and scientific simulations, need the computer to be unpredictable.
import java.util.Random;
Random random = new Random();
int n = random.nextInt(100); // 0 to 99 inclusive| call | returns | range |
|---|---|---|
| random.nextInt(100) | a pseudorandom int | 0 to 99 |
| random.nextInt(6) | a pseudorandom int | 0 to 5 |
| random.nextInt(6) + 1 | a die roll | 1 to 6 |
deterministic — Doing the same thing every time it runs.
pseudorandom — Produced by an algorithm that generates an unpredictable-looking sequence, which is not truly random.
Making a program nondeterministic turns out to be hard, because it is impossible for a computer to generate truly random numbers. But there are algorithms producing unpredictable sequences called pseudorandom numbers, and for most applications they are as good as random.
Notation
The range is the detail people get wrong, and it is the same off-by-one shape as everything else in this chapter.
Annotate
nextInt(n) returns a value from 0 to n-1 inclusive. The argument is the number of possible values, not the largest one.a[random.nextInt(a.length)] pick a random element safely.nextInt(6) + 1, because you want six values starting at 1 rather than at 0.If you generate a long series of random numbers, every value should appear approximately the same number of times. Testing that claim is exactly what the histogram in the next section does.
Worked example
This method creates an int array and fills it with random numbers between 0 and 99. The argument gives the desired size, and the return value is a reference to the new array.
public static int[] randomArray(int size) {
Random random = new Random();
int[] a = new int[size];
for (int i = 0; i < a.length; i++) {
a[i] = random.nextInt(100);
}
return a;
}| step | what happens |
|---|---|
| Random random = new Random(); | create one generator, outside the loop |
| int[] a = new int[size]; | create the array, all elements 0 |
| a[i] = random.nextInt(100); | overwrite each element with a value 0 to 99 |
| return a; | return a REFERENCE to the new array |
Create the Random object once, before the loop.
Why: One generator produces the whole sequence; creating a new one each pass is both wasteful and can repeat values.
Create the array with the requested size.
Why: new int[size] — the size is a parameter, so the caller decides.
Traverse and assign.
Why: The standard i < a.length traversal from the previous lesson, writing rather than reading.
Return the array.
Why: The return type is int[], and what is actually returned is a reference — the array itself is not copied.
Verify: Call randomArray(8) and print it: you should get something like {15, 62, 46, 74, 67, 52, 51, 10}.
Why: Then run the program again and confirm you get different values. If you got the same eight numbers twice, check that the Random object is created once rather than being re-created with the same seed.
Prediction
The argument is the number of possible values.
Random random = new Random();
int n = random.nextInt(6);| call | smallest | largest | how many values |
|---|---|---|---|
| nextInt(6) | 0 | 5 | 6 |
| nextInt(100) | 0 | 99 | 100 |
Predict first
What values can n take?
Correct: 0 through 5
Why: nextInt(n) returns a value from 0 to n-1 inclusive, so nextInt(6) gives six possible values starting at zero. That range is exactly the set of valid indexes for an array of length 6, which is what makes a[random.nextInt(a.length)] a safe way to pick a random element.
Concept
randomArray is the first method in the book to return an array, and it is worth being precise about what crosses the boundary.
Figure (svg): A variable in main with an arrow to the array that randomArray created and returned
The array was created inside the method, yet it survives the method returning — unlike the local variables in Lesson 4a, which vanish with their frame. What vanishes is the method's variable a; the array itself lives on because the caller now holds a reference to it. Chapter 10's garbage collection section explains when it is finally cleaned up.
Trap
A fresh generator every pass, which can produce repeated values.
int[] a = new int[size];
for (int i = 0; i < a.length; i++) {
Random random = new Random(); // inside the loop
a[i] = random.nextInt(100);
}| pass | what is created | risk |
|---|---|---|
| 1 | a new generator | seeded from the clock |
| 2 | another new generator | the clock may not have ticked |
| 3 | another | same seed, same first value |
A Random created with no argument is seeded from the system clock. Several created in quick succession can share a seed and therefore produce the same first value — so the array fills with repeats.
One generator, created before the loop.
public static int[] randomArray(int size) {
Random random = new Random();
int[] a = new int[size];
for (int i = 0; i < a.length; i++) {
a[i] = random.nextInt(100);
}
return a;
}| created | produces |
|---|---|
| one Random, before the loop | a single sequence, each call continuing it |
| a new Random each pass | several sequences that may all start the same |
This is the same principle as the accumulator in Lesson 7a: anything that must carry state across passes belongs outside the loop. A random generator's state is exactly what makes each call give a different answer.
Fill the middle
Produce a value from 1 to 6.
Fill in the blanks
int roll = random.nextInt(6) + 1;
Why: nextInt(6) gives six values, 0 through 5, and adding 1 shifts them to 1 through 6. Writing nextInt(7) would give seven possible values including 0 and 6, which is a seven-sided die with a zero on it — the classic off-by-one in random number generation.
Socratic
Think Java says it is impossible. Work out why.
Discussion prompt
A program is a sequence of instructions that produces the same result from the same inputs. Given that, why can no algorithm produce truly random numbers — and where could genuine randomness come from?
Hint: What would the algorithm have to do differently on two runs with the same starting state?
Answer:
An algorithm is deterministic by definition: same instructions, same state, same result. So any sequence it produces is completely determined by its starting state — the seed — and is reproducible by anyone who knows it.
Genuine randomness has to come from outside the computation: physical noise, timing jitter, radioactive decay. Operating systems collect such sources for cryptographic use, where predictability would be a security hole.
For games and simulations, pseudorandom is genuinely as good as random — the sequence passes statistical tests and nobody is trying to predict it. Knowing the difference matters when someone is: never use java.util.Random for anything security-related.
Definition probe
Inside the loop, or outside it?
Sort into buckets
Sort each declaration for a method that fills an array with random values.
Section
Section 7.7
Concept
If those random values were exam scores, a teacher might present them as a histogram — in statistics, a set of counters that keeps track of the number of times each value appears.
public static int inRange(int[] a, int low, int high) {
int count = 0;
for (int i = 0; i < a.length; i++) {
if (a[i] >= low && a[i] < high) {
count++;
}
}
return count;
}| part | role |
|---|---|
| int count = 0; | the accumulator, declared before the loop |
| a[i] >= low | low is INCLUDED in the range |
| a[i] < high | high is EXCLUDED |
| count++ | one more element in range |
| return count; | the reduced value |
This pattern should look familiar: it is another reduce operation from Lesson 7a. Notice that low is included with >= but high is excluded with < — this design keeps us from counting any score twice.
Picture it
Half-open ranges — include the bottom, exclude the top — are the same convention as substring in Lesson 6b, and they exist for the same reason.
Figure (svg): Two panels contrasting overlapping grade ranges that double-count with half-open ranges that tile exactly
With half-open ranges the boundaries line up: the top of one range is the bottom of the next, so the ranges tile the whole scale with no gaps and nothing counted twice.
Worked example
With inRange written, counting the five grade bands is five calls. This code is repetitive, and acceptable as long as the number of ranges is small.
int[] scores = randomArray(30);
int a = inRange(scores, 90, 100);
int b = inRange(scores, 80, 90);
int c = inRange(scores, 70, 80);
int d = inRange(scores, 60, 70);
int f = inRange(scores, 0, 60);| call | counts scores | traversals of the array |
|---|---|---|
| inRange(scores, 90, 100) | 90 to 99 | 1 |
| inRange(scores, 80, 90) | 80 to 89 | 1 |
| inRange(scores, 70, 80) | 70 to 79 | 1 |
| inRange(scores, 60, 70) | 60 to 69 | 1 |
| inRange(scores, 0, 60) | 0 to 59 | 1 |
| total | every score exactly once | 5 |
Write one call per band.
Why: Each returns the count for that band.
Check the boundaries line up.
Why: 90-100, 80-90, 70-80: the top of each is the bottom of the next, so nothing is missed or double counted.
Notice the cost.
Why: Each call traverses the entire array, so five bands means five full traversals.
Ask what happens with 100 bands.
Why: One hundred calls, and one hundred traversals. That is the problem the next idea solves.
Verify: Add the five counts together; the total must equal scores.length, which is 30.
Why: That check is worth doing every time you partition data into bands: if the total does not match, either a range is missing values or two ranges overlap.
Prediction
Low is included, high is excluded.
if (a[i] >= low && a[i] < high) {
count++;
}| score | counted? |
|---|---|
| 69 | no — below the low end |
| 70 | yes — low is included |
| 79 | yes |
| 80 | no — high is excluded |
Predict first
Which range of scores is counted?
Correct: 70 to 79
Why: The condition uses >= low so 70 is included, and < high so 80 is not. This half-open design is what lets the next band start at 80 without double-counting — the top of one range is exactly the bottom of the next.
Concept
Suppose you wanted to track how many times each individual score appears. With the inRange approach you would have to write 100 lines of code.
int count0 = inRange(scores, 0, 1);
int count1 = inRange(scores, 1, 2);
int count2 = inRange(scores, 2, 3);
// ...
int count99 = inRange(scores, 99, 100);| problem | consequence |
|---|---|
| 100 separate variables | you cannot loop over them |
| 100 lines of near-identical code | any change must be made 100 times |
| 100 traversals of the scores array | slow, and unnecessarily so |
What we need is a way to store 100 counters, preferably so we can use an index to access them. As Think Java puts it: wait a minute — that is exactly what an array does. This is the same realisation the chapter opened with, about 26 letter counters.
Trap
Inclusive at both ends. Every boundary score is counted twice.
// counting with <= at both ends
if (a[i] >= 80 && a[i] <= 90) countB++;
if (a[i] >= 90 && a[i] <= 100) countA++;| score | counted in B? | counted in A? | total |
|---|---|---|---|
| 85 | yes | no | once — correct |
| 90 | yes | yes | TWICE |
| 95 | no | yes | once — correct |
The counts add up to more than the number of scores, which is the symptom. Only the boundary values are wrong, so a test with no score exactly on a boundary would pass.
Half-open: include the low end, exclude the high.
if (a[i] >= low && a[i] < high) {
count++;
}| score | in [80, 90)? | in [90, 100)? | total |
|---|---|---|---|
| 85 | yes | no | once |
| 90 | no | yes | once |
| 95 | no | yes | once |
This is the third appearance of the half-open convention: array indexes 0 <= i < length, substring(start, end), and now counting ranges. Each time, the reason is the same — adjacent ranges tile exactly, with no gap and no overlap.
Definition probe
The three shapes from Lesson 7a.
Sort into buckets
Sort each method by its pattern.
Estimation
Each call to inRange walks the whole array.
Predict first
Using inRange to count each of 100 possible scores separately, how many times is the scores array traversed?
Correct: 100
Why: Every call to inRange contains its own loop over the entire array, so 100 calls means 100 full traversals — and the array is unchanged between them, so 99 of those are doing work that could have been done in the first. That inefficiency is precisely what the single-pass version in the next idea eliminates.
Explain it to yourself
A cheap test that catches both kinds of range error.
Discussion prompt
After partitioning scores into bands, adding the counts should give the total number of scores. Explain what a total that is too high tells you, and what a total that is too low tells you.
Hint: Two different mistakes, two different symptoms.
Answer:
Too high means overlap: some value fell into two bands and was counted twice. Too low means a gap: some value fell into no band at all and was counted zero times.
The check is valuable because it is cheap and it catches boundary errors, which are exactly the ones that survive casual testing — a test with no value on a boundary passes even when the ranges are wrong. Any time you partition data, add the parts up.
Section
Section 7.7, continued
Concept
The inRange version works but is not as efficient as it could be. For each score, we already know which range it falls in — the score itself. So we can use that value to increment the corresponding counter directly.
int[] counts = new int[100];
for (int i = 0; i < scores.length; i++) {
int index = scores[i];
counts[index]++;
}| scores[i] | index used | effect |
|---|---|---|
| 62 | 62 | counts[62]++ |
| 15 | 15 | counts[15]++ |
| 62 | 62 | counts[62]++ again — now 2 |
| 99 | 99 | counts[99]++ |
Each pass selects one element from scores and uses it as an index to increment the corresponding element of counts. Because this traverses the scores array only once, it is much more efficient.
Picture it
This is the conceptual jump of the chapter. An index has been a position until now; here it is a name, and the value chooses which counter to bump.
Figure (svg): A score of 62 shown selecting the counter at index 62 in a counts array
It works because the counts array was made with exactly as many elements as there are possible values — new int[100] for scores 0 to 99. The value and the index range line up by construction.
Worked example
Both produce the same histogram. The difference is how much work they do, and it is not a small difference.
// version 1: correct, but 100 traversals
int[] counts = new int[100];
for (int i = 0; i < counts.length; i++) {
counts[i] = inRange(scores, i, i + 1);
}
// version 2: one traversal
int[] counts = new int[100];
for (int i = 0; i < scores.length; i++) {
counts[scores[i]]++;
}| version | loops over | traversals of scores | total steps for 30 scores |
|---|---|---|---|
| 1 — inRange | counts (100) | 100 | about 3,000 |
| 2 — value as index | scores (30) | 1 | 30 |
Look at what version 1 loops over.
Why: The counts array — one pass per counter, and each pass calls inRange which walks all the scores.
Look at what version 2 loops over.
Why: The scores array — one pass total, each score bumping one counter.
Notice the loop variable's meaning changed.
Why: In version 1, i is a score value being asked about. In version 2, i is a position in scores, and the value found there is the index.
Count the steps.
Why: Version 1 does scores.length times counts.length work; version 2 does scores.length. For 30 scores that is 3,000 against 30.
Verify: Run both and confirm the counts arrays are identical — Arrays.equals(a, b) will tell you in one line.
Why: Same answer, a hundredth of the work. This is the first place in the book where two correct programs differ enormously in cost, and it is the idea Chapter 12 develops when it compares sequential and binary search.
Prediction
The single-pass histogram, run on four scores.
int[] scores = {62, 15, 62, 99};
int[] counts = new int[100];
for (int i = 0; i < scores.length; i++) {
counts[scores[i]]++;
}| score | counter incremented | counts[62] after |
|---|---|---|
| 62 | counts[62] | 1 |
| 15 | counts[15] | 1 |
| 62 | counts[62] | 2 |
| 99 | counts[99] | 2 |
Predict first
What is counts[62]?
Correct: 2
Why: The value 62 appears twice in scores, and each occurrence increments the counter at index 62, so it ends at 2. The counter's index is the score it counts — that correspondence is what makes the whole technique work.
Concept
Version 1 is worth one more look, because it uses the same variable for three different jobs in three consecutive positions.
for (int i = 0; i < counts.length; i++) {
counts[i] = inRange(scores, i, i + 1);
}| where i appears | what it means there |
|---|---|
| counts[i] | an index into the counts array |
| inRange(scores, i, ...) | the low end of the range — a score value |
| ..., i + 1) | the high end — one more than that score |
Think Java points this out explicitly: notice that we are using the loop variable i three times. It works because the counter at index i counts exactly the score i — the correspondence between index and value is the whole design. Version 2 exploits that correspondence directly instead of restating it.
Trap
The value is used as an index without checking it fits.
int[] counts = new int[100];
for (int i = 0; i < scores.length; i++) {
counts[scores[i]]++; // what if a score is 100? or -1?
}| a score of | index used | outcome |
|---|---|---|
| 0 | 0 | fine |
| 99 | 99 | fine — the last counter |
| 100 | 100 | ArrayIndexOutOfBoundsException |
| -1 | -1 | ArrayIndexOutOfBoundsException |
The technique depends entirely on every value being a legal index. randomArray guarantees 0 to 99, so this code is safe for that data — but it would break instantly on scores read from a file.
Size the counter array to the range of possible values, and validate anything from outside.
int[] counts = new int[100]; // one counter per possible score
for (int score : scores) {
if (score >= 0 && score < counts.length) {
counts[score]++;
} else {
System.err.println("out of range: " + score);
}
}| guarantee needed | how it is met |
|---|---|
| values are 0 or more | nextInt never returns negative — or check it |
| values are below counts.length | nextInt(100) with new int[100] — or check it |
| data from outside the program | must be validated, as in Lesson 5b |
The design rule: the counter array's length must be the number of possible values. When you control the data that is a matter of construction; when you do not, it is a guard clause.
Discrimination
The two versions loop over different arrays, and that is the whole difference.
Sort into buckets
Sort each loop header by which array it traverses.
Scale up
Compare the two versions as the amount of data increases.
Step through it
If the number of scores doubles, what happens to each version's cost?
Both double — the counter array's size is fixed, so version 1's cost is scores times 100 and version 2's is scores. Version 1 is a hundred times slower at every size, which is a constant factor rather than a different growth rate. Constant factors of 100 still decide whether a program is usable.
Real world
This technique — let the data choose the slot — is far more general than histograms.
Discussion prompt
Counting scores works because a score is already a number in the right range. Where else could you use a value directly as an index, and what would you do when the values are not numbers at all?
Hint: The next idea converts letters into indexes.
Answer:
Anywhere the values form a small known range: counting die rolls, tallying votes for numbered candidates, counting how many items fall in each month of the year.
When the values are not numbers, you map them to numbers. The next section does exactly that with letter - 'a', turning 26 letters into indexes 0 to 25.
And when the range is huge or unknown — arbitrary words, say — an array of counters stops being practical, and you need a different structure. That structure is a hash map, and it is doing conceptually the same thing: computing an index from a value.
Section
Section 7.8
Concept
Since traversing arrays is so common, Java provides an alternative syntax that makes the code more compact. It is called the enhanced for loop, also known as the for each loop.
// the standard form
for (int i = 0; i < values.length; i++) {
int value = values[i];
System.out.println(value);
}
// the enhanced form
for (int value : values) {
System.out.println(value);
}| part | meaning |
|---|---|
| int value | a variable that takes each element in turn |
| : | read as "in" |
| values | the array to traverse |
| the whole header | "for each value in values" |
The single line for (int value : values) replaces the first two lines of the standard form. It hides the details of iterating each index, and focuses on the values themselves.
Notation
The convention makes the loop read as English, and what the form leaves out is exactly what decides when you can use it.
Annotate
i, which is what makes the loop shorter and also what limits it.i < length wrong, because you do not write it — a whole class of off-by-one error disappears.Enhanced for loops often make the code more readable, especially for accumulating values. The question to ask before using one is simply: do I need the index?
Worked example
Using the enhanced for loop and removing the temporary variable, the histogram code becomes about as short as it can be.
// before
int[] counts = new int[100];
for (int i = 0; i < scores.length; i++) {
int index = scores[i];
counts[index]++;
}
// after
int[] counts = new int[100];
for (int score : scores) {
counts[score]++;
}| line | before | after |
|---|---|---|
| loop header | index, condition, update | one clause |
| temporary | int index = scores[i]; | the loop variable IS the value |
| the work | counts[index]++; | counts[score]++; |
| total lines in the body | 2 | 1 |
Replace the header with the enhanced form.
Why: for (int score : scores) — the variable now holds the element rather than its position.
Delete the temporary.
Why: int index = scores[i]; existed only to name the element, and the loop variable already does.
Note that counts is still indexed normally.
Why: The enhanced loop traverses scores; counts is being indexed by value, which is unaffected.
Check it still reads correctly.
Why: For each score in scores, increment the counter for that score. The code now says what the sentence says.
Verify: Run both versions on the same scores and compare the counts arrays with Arrays.equals.
Why: Identical. The enhanced loop changed the notation, not the algorithm — and notice that the version that is easier to read is also the one where the off-by-one cannot happen.
Prediction
The loop variable is assigned to.
int[] a = {1, 2, 3};
for (int value : a) {
value = 0;
}| what is assigned | effect on a |
|---|---|
| the loop variable | none — it is a copy of the element |
| a[i] | would change the array — but is not written here |
Predict first
What does a contain afterwards?
Correct: {1, 2, 3} — unchanged
Why: The enhanced for loop assigns a copy of each element to the loop variable, so writing to that variable changes the copy and not the array. This is the same copying that happens in parameter passing, and it is why the enhanced form can read an array but never modify it.
Concept
Enhanced for loops are not helpful when you need to refer to the index, as in search operations.
for (double d : array) {
if (d == target) {
// array contains d, but we don't know where
}
}| you need to | enhanced for? | why |
|---|---|---|
| visit every element and accumulate | yes | the index is never used |
| return the position of a match | no | there is no index to return |
| modify the array's elements | no | the loop variable is a copy |
| traverse backwards | no | it always goes forwards |
| compare a[i] with a[i + 1] | no | you need two positions at once |
The first row is the common case, which is why the enhanced form is worth reaching for by default. The remaining four are exactly the situations where the standard form is not merely preferable but necessary.
Trap
The array is unchanged. The loop variable holds a copy.
int[] a = {1, 2, 3};
for (int value : a) {
value = value * 2; // changes the copy only
}
// a is still {1, 2, 3}| pass | value | a[i] |
|---|---|---|
| 1 | 1, then 2 | 1 — unchanged |
| 2 | 2, then 4 | 2 — unchanged |
| 3 | 3, then 6 | 3 — unchanged |
This is Lesson 4a's parameter passing all over again: the loop variable is assigned a copy of each element, so changing it leaves the array alone. It compiles and silently does nothing.
To modify elements, use the standard form and assign through the index.
int[] a = {1, 2, 3};
for (int i = 0; i < a.length; i++) {
a[i] = a[i] * 2; // writes into the array
}
// a is now {2, 4, 6}| form | reads elements? | can write them? |
|---|---|---|
| enhanced for | yes | no — the variable is a copy |
| standard for | yes | yes — a[i] on the left of an assignment |
The rule of thumb: enhanced for to read, standard for to write. The transform pattern from Lesson 7a always needs the standard form, for exactly this reason.
Definition probe
Ask whether you need the index.
Sort into buckets
Sort each task.
Fill the middle
Sum an int array.
Fill in the blanks
int total = 0;
for (int n : numbers) +=} n;
}
Why: The colon is read as "in", so the header says for each n in numbers. The accumulator is still declared before the loop and updated with += — the enhanced form changes only how you get at each element, not the reduce pattern itself.
Counterexample
It is a deliberate design decision. Ask what it prevents.
Discussion prompt
Java could have made the enhanced for loop's variable write through to the array. It does not. What would be ambiguous or dangerous if it did — and what does the current design let you assume when you read one?
Hint: Think about what you know the moment you see the enhanced form.
Answer:
The current design means seeing an enhanced for loop tells you the array is not being modified. That is real information for a reader, and it disappears if the variable could write.
It also keeps the form consistent: the enhanced loop works over things other than arrays, including collections where 'writing to the current element' may have no meaning at all.
So the restriction is not an oversight but a guarantee. The shorter form is also the one that promises less — and when you need the guarantee to be weaker, the standard form is there.
Section
Section 7.9
Concept
Now the problem the chapter opened with. A word is a doubloon if every letter that appears in it appears exactly twice. The plan is an array of 26 counters — and the missing piece is how to get from a letter to an index.
int[] counts = new int[26];
String lower = s.toLowerCase();
for (int i = 0; i < lower.length(); i++) {
char letter = lower.charAt(i);
int index = letter - 'a';
counts[index]++;
}| letter | letter - 'a' | counter incremented |
|---|---|---|
| 'a' | 0 | counts[0] |
| 'b' | 1 | counts[1] |
| 'm' | 12 | counts[12] |
| 'z' | 25 | counts[25] |
Java allows you to perform arithmetic on characters. Subtracting 'a' from a lowercase letter gives its position in the alphabet: if letter is 'a' the index is 0, if 'b' then 1, and so on.
Picture it
It looks like a trick and it is arithmetic. Lesson 6a's point that a char is a number underneath is what makes it possible.
Figure (svg): A row showing letters a through e with their Unicode code points and the result of subtracting the code point of a
This works because the lowercase letters are consecutive in Unicode — the same property that let for (char c = 'a'; c <= 'z'; c++) walk the alphabet in Lesson 6a. It is a fact about the encoding, not a general truth about alphabets.
Worked example
Two traversals: one to build the histogram, one to check it. Neither is complicated, and together they solve the problem the chapter opened with in a single pass over the word.
public class Doubloon {
public static boolean isDoubloon(String s) {
// count the number of times each letter appears
int[] counts = new int[26];
String lower = s.toLowerCase();
for (char letter : lower.toCharArray()) {
int index = letter - 'a';
counts[index]++;
}
// determine whether the given word is a doubloon
for (int count : counts) {
if (count != 0 && count != 2) {
return false;
}
}
return true;
}
public static void main(String[] args) {
System.out.println(isDoubloon("Mama")); // true
System.out.println(isDoubloon("Lama")); // false
}
}| word | counts after the first loop | verdict |
|---|---|---|
| "Mama" | m appears 2, a appears 2, rest 0 | true |
| "Lama" | l appears 1, a appears 2, m appears 1 | false |
| "" | all zeros | true — vacuously |
Lowercase the string first.
Why: toLowerCase so that 'A' and 'a' count as the same letter — and so that letter - 'a' is in range.
Build the histogram with an enhanced for loop.
Why: lower.toCharArray() is needed because the enhanced form does not work directly on a String.
Check every counter.
Why: If a count is neither 0 nor 2, the word is not a doubloon and we can return immediately.
Return true if the second loop finishes.
Why: Reaching the end means every count was 0 or 2 — which is the definition.
Verify: Expect isDoubloon("Mama") to be true and isDoubloon("Lama") to be false.
Why: Then trace "Mama": lowercased it is "mama", so m appears twice and a appears twice, every other counter is 0, and every count is 0 or 2. For "lama" the l and the m each appear once, so the first non-conforming count returns false.
This example uses methods, if statements, for loops, arithmetic and logical operators, integers, characters, strings, booleans and arrays. It is worth a moment to notice how much that is.
Prediction
Character arithmetic.
char letter = 'd';
int index = letter - 'a';| character | code point |
|---|---|
| 'a' | 97 |
| 'd' | 100 |
| 100 - 97 | 3 |
Predict first
What is index?
Correct: 3
Why: Subtracting the code point of 'a' from that of 'd' gives 3, which is d's zero-based position in the alphabet — a is 0, b is 1, c is 2, d is 3. The subtraction works because the lowercase letters occupy consecutive code points in Unicode.
Concept
The checking loop returns from inside the loop and again after it. Both are deliberate, and the pattern is the same as search in Lesson 7a.
for (int count : counts) {
if (count != 0 && count != 2) {
return false; // one bad count is enough
}
}
return true; // every count was fine| situation | which return runs | why |
|---|---|---|
| a count is 1 | the one inside the loop | one counterexample settles it |
| a count is 3 | the one inside the loop | same |
| every count is 0 or 2 | the one after the loop | the loop finished without finding a problem |
The asymmetry is worth naming: to prove false you need one counterexample; to prove true you must check everything. That is why the false case can return early and the true case cannot — and it is the same shape as returning -1 only after a search loop completes.
Trap
An uppercase letter produces a negative index.
int[] counts = new int[26];
for (char letter : s.toCharArray()) { // no toLowerCase
int index = letter - 'a';
counts[index]++;
}| letter | code point | letter - 'a' | outcome |
|---|---|---|---|
| 'a' | 97 | 0 | fine |
| 'M' | 77 | -20 | ArrayIndexOutOfBoundsException |
| 'Z' | 90 | -7 | ArrayIndexOutOfBoundsException |
Uppercase letters come before lowercase in Unicode, so subtracting 'a' from one gives a negative number. isDoubloon("Mama") would throw on its very first character.
Lowercase the whole string first, which handles case-insensitivity and the range at once.
String lower = s.toLowerCase();
for (char letter : lower.toCharArray()) {
int index = letter - 'a';
counts[index]++;
}| what toLowerCase fixes | how |
|---|---|
| 'M' and 'm' should be the same letter | both become 'm' |
| 'M' - 'a' would be negative | 'm' - 'a' is 12 |
| every index is 0 to 25 | because every letter is now lowercase |
One call solves two problems, which is why the book's instructions say to ignore case, invoke the toLowerCase method before checking. Note that a string containing a space or a digit would still produce an out-of-range index — the method assumes letters only.
Prediction
Lowercase it first, then count.
isDoubloon("Mama")| letter | count |
|---|---|
| m | 2 |
| a | 2 |
| every other letter | 0 |
Predict first
What does the method return?
Correct: true — every letter that appears appears exactly twice
Why: Lowercased, "Mama" becomes "mama": m appears twice and a appears twice, and every other counter is 0. The check accepts counts of 0 or 2, so all 26 counters pass and the method returns true. The uppercase M causes no problem precisely because toLowerCase runs first.
Error analysis
Five decisions in fifteen lines, each with a reason.
Annotate
new int[26] — one counter per letter of the alphabet, and every element starts at 0, so no initialisation loop is needed.toLowerCase() — does two jobs: it makes the check case-insensitive as the problem requires, and it guarantees every index is 0 to 25.toCharArray() — the enhanced for loop does not work on a String, so the string is converted to an array of characters first.letter - 'a' — character arithmetic, mapping each letter to its alphabet position. This is the step that lets a value choose its own counter.count != 0 && count != 2 — a letter may be absent (0) or appear twice (2); anything else disqualifies the word. Note that 0 must be allowed, since 24 of the 26 counters are usually zero.return false inside, return true after — one bad count settles it immediately, but confirming a doubloon requires checking every counter.Nothing here is advanced. What makes it satisfying is that it is the whole chapter — arrays, traversal, using a value as an index, and the enhanced for loop — solving a problem that was impractical at the start of it.
Edge cases
Test the method at its edges.
Discussion prompt
What does isDoubloon("") return? And what would happen with isDoubloon("hello world")? Reason from the code rather than from intuition.
Hint: For the second one, work out what ' ' - 'a' is.
Answer:
The empty string returns true. The first loop runs zero times, so every counter stays 0, and the check accepts 0 — so every counter passes. Whether vacuously true is the right answer is a question about the specification rather than the code.
"hello world" throws. The space has code point 32, so ' ' - 'a' is -65, and counts[-65]++ is an ArrayIndexOutOfBoundsException.
So the method silently assumes its input is letters only. That assumption is fine for the exercise and would be a bug in anything handling real text — which is the same lesson as Lesson 5b's validation: a method that works on the data you tested is not the same as a method that is correct.
Comparison
Same output, very different cost. Fill the blanks.
Comparison matrix
| with inRange | value as index | |
|---|---|---|
| the loop goes over | the counts array | the scores array |
| traversals of scores | one per counter — 100 | one, total |
| the loop variable is | a score value being asked about | a position in scores |
| steps for 30 scores | about 3,000 | 30 |
Both are correct. The second is a hundred times cheaper because it stops asking how many scores equal this? and starts saying this score belongs here.
Pattern
Counting anything into buckets is the same four lines, whatever the values are — as long as you can turn a value into an index.
// 1. one counter per possible value
int[] counts = new int[NUMBER_OF_POSSIBLE_VALUES];
// 2. one pass over the data
for (int value : data) {
counts[value]++; // the value names its own counter
}
// for letters, map them into range first:
for (char letter : s.toLowerCase().toCharArray()) {
counts[letter - 'a']++;
}| requirement | why |
|---|---|
| the counter array's length is the number of possible values | so every value is a legal index |
| every value is 0 or more and below that length | otherwise ArrayIndexOutOfBoundsException |
| new int[] initialises to zero | so the counters are ready without a setup loop |
| one pass, not one per counter | the value tells you which counter, so no searching is needed |
nextInt(n) returns 0 to n-1 — the same range as the indexes of an array of length n.letter - 'a' maps a lowercase letter to 0 through 25.Check
Work it out before you click.
Check your understanding
Which expression produces a random value from 1 to 10 inclusive?
Answer: A
Why: nextInt(10) gives ten possible values, 0 through 9, and adding 1 shifts them to 1 through 10. The argument is the number of possible values rather than the maximum, which is the same convention as an array's length.
Check
Work it out before you click.
int[] a = {5, 10, 15};
int total = 0;
for (int n : a) {
total += n;
n = 0;
}| pass | n | total after | a[i] |
|---|---|---|---|
| 1 | 5 | 5 | 5 — unchanged |
| 2 | 10 | 15 | 10 — unchanged |
| 3 | 15 | 30 | 15 — unchanged |
Check your understanding
What is total, and what does a contain afterwards?
Answer: A
Why: The accumulator adds each element as it is visited, giving 30. Assigning to the loop variable changes only the copy it holds, so the array is untouched — the enhanced for loop can read an array but never modify it.
Check
Work it out before you click.
int[] counts = new int[26];
String lower = "bee".toLowerCase();
for (char letter : lower.toCharArray()) {
counts[letter - 'a']++;
}| letter | index | counter |
|---|---|---|
| b | 1 | counts[1] becomes 1 |
| e | 4 | counts[4] becomes 1 |
| e | 4 | counts[4] becomes 2 |
Check your understanding
What are counts[1] and counts[4]?
Answer: A
Why: 'b' - 'a' is 1 and 'e' - 'a' is 4, so the b increments counter 1 once and the two e's increment counter 4 twice. The mapping is zero-based: a is 0, b is 1, and so on up to z at 25.
Real world
The histogram technique — one counter per possible value, one pass over the data — is one of the most reused ideas in programming.
Discussion prompt
You have used it for exam scores and for letters. Where else would this exact technique work, and what property must the data have for it to be possible at all?
Hint: Think about what the counter array's length has to be.
Answer:
It works wherever the values come from a small, known, countable range: die rolls, days of the week, pixel brightness values 0 to 255, DNA bases, votes for numbered candidates.
The required property is that you can map each value to an index in a fixed range without gaps. Scores are already indexes; letters need - 'a'; anything else needs a mapping you invent.
When the range is huge or unknown — arbitrary words, or user IDs — an array of counters becomes impractical and you reach for a hash map instead. But a hash map is doing the same thing underneath: computing an index from a value. Understanding the array version is what makes the map version make sense.
Commit first
Commit to an answer and to your confidence.
Predict first
Can an enhanced for loop be used to double every element of an array?
Correct: No — the loop variable is a copy, so assigning to it does not change the array
Why: The enhanced for loop assigns a copy of each element to the loop variable, exactly as parameter passing copies an argument in Lesson 4a. Writing to that variable changes the copy and the array is untouched — and the code compiles and silently does nothing, which makes it a logic error. To modify elements you need the standard form and a[i] on the left of an assignment. The rule worth keeping: enhanced for to read, standard for to write.
Explain it
Two minutes, out loud.
Discussion prompt
Explain to a classmate why counting scores with counts[scores[i]]++ is so much faster than calling inRange a hundred times. They will say both loops look about the same length. Answer that directly.
Hint: Count what is nested inside what.
Answer:
They look the same because you can only see the outer loop. The inRange version has a second loop hidden inside the method — every call walks the entire scores array. So a hundred calls means a hundred full traversals.
The other version has no hidden loop. It walks the scores once, and each score bumps one counter directly, because the score IS the index. There is nothing to search for.
The general lesson worth adding: a method call can hide a loop. Judging cost by the code you can see is exactly how this mistake gets made, and it is why Chapter 12 spends time on counting steps properly.
Exit ticket
One question before you close the deck.
Predict first
Why does letter - 'a' give the position of a lowercase letter in the alphabet?
Correct: Because the lowercase letters occupy consecutive code points, so the difference is the offset from 'a'
Why: A char is a number underneath — its Unicode code point — and the lowercase letters run consecutively from 97 for 'a' to 122 for 'z'. Subtracting 97 from any of them therefore gives 0 through 25. Note that this is a property of the encoding rather than a rule about alphabets: it is the same fact that let for (char c = 'a'; c <= 'z'; c++) walk the alphabet in Lesson 6a, and it is why uppercase input must be lowercased first — 'M' would give a negative index.
Connect it up
One page, from memory.
Draw it
Draw a counts array of ten boxes numbered 0 to 9, and trace the scores {3, 7, 3, 3, 7} into it one at a time, drawing an arrow from each score to the counter it increments. Then write the same loop twice — once with a standard for and once with an enhanced for — and mark which one could modify the scores array. Finally, write letter - 'a' and explain in one sentence why it gives 0 for 'a'.
Recap
Four sections that finish the chapter and answer the question it opened with: how to count 26 things without declaring 26 variables.
| if you remember one thing | it is this |
|---|---|
| about counting | let the value choose its own counter |
| about the enhanced for | read with it, write without it |
| about characters | a char is a number, so you can do arithmetic on it |
java.util.Random produces pseudorandom numbers, which are not truly random.nextInt(n) returns 0 to n-1 — the same range as the indexes of an array of length n.for each value in values and hides the index — so it cannot search or write.letter - 'a' maps a lowercase letter to 0 through 25, because the letters are consecutive in Unicode.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.