Turning almostMergeSort into real merge sort by replacing one call with a recursive one, the two base cases that make it terminate, the static-context error messages spelled out at last, and ArrayList — a collection that grows and shrinks — used to build the piles for a game of War. Follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.7-13.10, pp. 224-230, cross-referenced against The Java Tutorials — Understanding Class Members (static).
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 13 · Objects of Arrays
Sections 13.7-13.10 · pp. 224-230
Objectives
This lesson follows Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.7-13.10, pp. 224-230. Everything on these slides can be checked against those pages.
1. Add base cases to a recursive sort and explain why zero and one cards are both worth handling.
2. Apply the leap of faith to a recursive method rather than tracing it.
3. Read a UML class diagram, including the notation for private and static members.
4. Explain both non-static-context compiler errors and say what causes each.
5. Declare an ArrayList of a given type and use add, remove and isEmpty.
6. Explain what a wrapper method is and why Pile is built from one.
Warm-up
Two things you will need, one of them from six chapters ago.
Discussion prompt
From Lesson 8a: what two things does every recursive method need, and what is the leap of faith? And from Lesson 13a: what three steps make up merge sort?
Hint: A base case and a smaller problem. And divide, sort, merge.
Answer:
A recursive method needs a base case and a recursive call on a smaller problem. The leap of faith is assuming the recursive call works rather than tracing into it.
Merge sort is divide, sort the halves, merge. Lesson 13a's almostMergeSort used selection sort for the middle step. We can make it more efficient if we use mergeSort instead — but that means we have to make it recursive.
Concept
almostMergeSort already divides, sorts and merges. Making it a real merge sort changes exactly one line — and that is the point Section 13.7 wants you to feel.
Figure (svg): Two panels showing almostMergeSort calling selectionSort against mergeSort calling itself
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 13 (Objects of Arrays), Sections 13.7-13.10, pp. 224-230 — Section 13.7 opens on printed page 224.
Section
Section 13.7
Concept
To make mergeSort work recursively, you have to add a base case; otherwise it repeats forever. The simplest one is a deck of a single card.
public Deck mergeSort() {
// if the deck has 0 or 1 cards, return it
// otherwise, divide the deck into two subdecks
// sort the subdecks using mergeSort
// merge the subdecks
// return the result
}| deck size | what happens | why |
|---|---|---|
| 0 cards | return it | no cards, so they cannot be out of order |
| 1 card | return it | one card cannot be out of order |
| 2 or more | divide, sort each, merge | the recursive case |
The simplest base case is a subdeck with one card. If there is only one card, it can't be out of order, so we consider it sorted. And if it is already sorted, we can just return it.
Notation
One card is enough to make the recursion terminate. The book adds a second base case anyway.
Annotate
high - low + 1 warning. An empty subdeck is what a small mistake produces.A base case that costs one line and covers a case you believe impossible is usually worth writing. Zero and one are the two sizes at which sortedness is free.
Worked example
As usual, there are two ways to think about recursive programs: you can follow the flow of execution, or you can make the leap of faith. This example should push you toward the second.
// almostMergeSort:
// sort the subdecks using selectionSort <- you trust this
// mergeSort:
// sort the subdecks using mergeSort <- trust this the same way| call | you assume | why that is fair |
|---|---|---|
| selectionSort(half) | it returns a sorted half | you debugged it |
| mergeSort(half) | it returns a sorted half | same claim, same method |
| merge(a, b) | it returns them combined in order | you debugged it |
Notice how you read almostMergeSort.
Why: When you use selectionSort to sort the subdecks, you don't feel compelled to follow the flow of execution. You assume it works because you already debugged it.
Notice what actually changed.
Why: When you make mergeSort recursive, you just replace one sorting algorithm with another. There is no reason to read the program differently.
Accept the one caveat.
Why: Well, almost. You have to think about the base cases and make sure that you reach them.
Then stop tracing.
Why: Other than that, writing the recursive version should be no problem.
Verify: Ask of your own recursive call only this: does it get a smaller deck, and will it reach a base case?
Why: If the answer is yes twice, the leap of faith is justified. Tracing into the call adds nothing — you would only be re-checking a method you have already accepted, which is the point Lesson 8a made and this example makes concrete.
Prediction
The method divides and recurses, forever.
public Deck mergeSort() {
// divide into two subdecks
// sort each with mergeSort
// merge and return
}| depth | deck size |
|---|---|
| 0 | 52 |
| 6 | 1 |
| 7 | 1 — and still recursing |
Predict first
What happens?
Correct: It recurses forever and throws a StackOverflowError
Why: Without a base case there is nothing to stop the descent, so each call adds a frame to the stack until it overflows — the same failure as the recursive countdown in Lesson 8a. Otherwise it repeats forever is Think Java's own phrasing.
Concept
Each call splits its deck and calls itself twice. The tree that produces is where the log₂ n comes from.
Figure (svg): A call tree showing a deck of 8 splitting into 4 and 4, then 2s, then single cards
Each level of the tree handles all n cards, and there are log₂ n levels — which is exactly the n log₂ n from Lesson 13a, now visible as a shape. For 52 cards the tree is about six deep.
Trap
Recursing on the same deck, or on a base case you never reach.
public Deck mergeSort() {
// no base case at all
Deck a = this.subdeck(0, size / 2);
Deck b = this.subdeck(size / 2, size - 1); // overlaps!
return merge(a.mergeSort(), b.mergeSort());
}| size | a | b | shrinking? |
|---|---|---|---|
| 4 | 0..2 (3 cards) | 2..3 (2 cards) | yes, but a card is duplicated |
| 2 | 0..1 (2 cards) | 1..1 (1 card) | a never shrinks |
| result | — | — | StackOverflowError |
Two faults at once: no base case, and a split whose first half can equal the whole. Lesson 8a's rule is that each call must get strictly smaller, and subdeck(0, size / 2) does not guarantee that.
Base cases first, and halves that are strictly smaller.
public Deck mergeSort() {
if (size <= 1) {
return this; // base cases: 0 and 1
}
int mid = size / 2;
Deck a = this.subdeck(0, mid - 1);
Deck b = this.subdeck(mid, size - 1);
return merge(a.mergeSort(), b.mergeSort());
}| size | a | b | both smaller? |
|---|---|---|---|
| 4 | 0..1 (2) | 2..3 (2) | yes |
| 2 | 0..0 (1) | 1..1 (1) | yes — both hit the base case |
| 1 | — | — | base case, returns immediately |
mid - 1 and mid are adjacent and disjoint, so the halves cover every card exactly once and both are strictly smaller than the original. Check the size-2 row: that is the case where a careless split hangs.
Definition probe
Which deck sizes are handled directly?
Sort into buckets
Sort each size.
Fill the middle
The only line that changes from almostMergeSort.
Fill in the blanks
// if the deck has 0 or 1 cards, return it
// otherwise, divide the deck into two subdecks
// sort the subdecks using mergeSort
// merge the subdecks and return the result
Why: Replacing selectionSort with mergeSort is the whole change, plus the base cases. That is the point of the section: you are substituting one sorting method for another, and the fact that it happens to be the method you are writing changes nothing about how you should read the code.
Explain it to yourself
It looks like circular reasoning.
Discussion prompt
Assuming mergeSort works while writing mergeSort sounds like assuming what you want to prove. Why is it legitimate?
Hint: What is different about the deck the inner call gets?
Answer:
Because the recursive call gets a strictly smaller deck. You are not assuming the method works on this deck — you are assuming it works on a smaller one.
And smaller decks eventually reach the base cases, which are handled directly and obviously correctly. The chain of assumptions is finite and it terminates in something you can check.
That is induction, and it is why Think Java's one caveat is exactly the right one: you have to think about the base cases and make sure that you reach them. Everything else really is a substitution.
Section
Section 13.8
Concept
Figure 13.3 shows a UML class diagram for Deck, including the instance variable and the methods so far. In UML diagrams, private attributes and methods begin with a minus sign, and static methods are underlined.
Figure (svg): A UML class diagram for Deck showing the private cards attribute and its public and static methods
| notation | means |
|---|---|
| - | private |
| + | public |
| underlined | static |
| listed above the line | attributes — instance variables |
The helper methods randomInt and merge are static, because they do not read or write any instance variables. All other methods are instance methods, because they access the instance variable, cards. That is the whole rule.
Notation
When you have static methods and instance methods in the same class, it is easy to get them confused. Here are both mistakes and both messages.
Annotate
Deck with a capital D is a class, and deck with a lowercase d is an object. Calling an instance method on the class is the first error.this in a static method. A static method is not invoked on an object, so there is no object for this to mean.cards is an instance variable, so it is non-static; therefore you can't access it from a static method.Both messages say the same thing in different words: something that needs an object is being used where there is no object. Once you read them that way they stop being cryptic.
Worked example
To invoke an instance method, you need an instance. The capital letter is the tell.
Deck deck = new Deck();
deck.print(); // correct
Deck.print(); // wrong!| call | legal? | why |
|---|---|---|
| deck.print() | yes | deck is an object, print is an instance method |
| Deck.print() | no | Deck is a class; print needs an object |
| Deck.randomInt(0, 51) | yes | randomInt is static |
| deck.randomInt(0, 51) | legal, poor style | see below |
Look at the capital letter.
Why: Deck with a capital D is a class, and deck with a lowercase d is an object.
An instance method needs the object.
Why: deck.print() — the object supplies this.cards.
A static method should be called on the class.
Why: Deck.randomInt(0, 51) says clearly that no object is involved.
The last row is the interesting one.
Why: This is legal, but it is not considered good style, because someone reading this code would expect randomInt to be an instance method.
Verify: Write Deck.print(); and read the compiler error; then fix it and read nothing.
Why: Java allows deck.randomInt(...) and it does exactly the same thing as Deck.randomInt(...) — the object is ignored. It is allowed and it misleads the reader, which is why the book calls it out as style rather than as an error.
Prediction
print is an instance method.
Deck deck = new Deck();
Deck.print();| print needs | Deck is |
|---|---|
| an object, for this.cards | a class |
Predict first
What happens?
Correct: A compile error: non-static method print() cannot be referenced from a static context
Why: print reads this.cards, so it needs an object, and Deck is a class rather than an object. The compiler cannot guess which deck you meant — the fact that a deck variable exists nearby is irrelevant, because the call named the class.
Concept
Deciding whether a method should be static is a single question, and Lesson 12a's variables table answers it.
| does it read or write an instance variable? | then it is | call it on |
|---|---|---|
| yes | an instance method | an object |
| no | a static method | the class |
| examples that do | print, shuffle, subdeck, swapCards | deck |
| examples that do not | randomInt, merge | Deck |
merge is static even though it takes two Decks — the decks arrive as parameters, so it never touches this.cards. That is the same reason Math.max is static: it works on what you give it, not on what it belongs to.
Trap
A static method has no object, so it has no this.
private static Deck merge(Deck d1, Deck d2) {
return this.cards; // wrong!
}| instance method | static method | |
|---|---|---|
| invoked on | an object | the class |
| has this? | yes | no |
| can read cards? | yes | no |
| error if it tries | — | Non-static variable this cannot be referenced |
In general, you can't use this in a static method, because a static method is not invoked on an object. There is nothing for this to refer to.
Take what you need as a parameter.
private static Deck merge(Deck d1, Deck d2) {
// use d1.cards and d2.cards - both arrived as parameters
...
}
// or make it an instance method, if it really operates on this deck| need | fix |
|---|---|
| a deck to work on | take it as a parameter |
| this deck specifically | make the method non-static |
| nothing from any object | leave it static |
A static method inside the class may still read another object's private variables — d1.cards is legal because private means within this class, not within this object. That is why merge can be static and still see both decks' arrays.
Definition probe
The question is whether it touches this.cards.
Sort into buckets
Sort each Deck method.
Matching
Two messages, two mistakes.
Match the pairs
Why: Both errors say the same thing: something needing an object was used where there is no object. Once you translate static context as no object here, the messages become straightforward rather than cryptic — which is exactly what the section sets out to fix.
Socratic
Java accepts it.
Discussion prompt
Calling a static method through an object compiles and works. Think Java still calls it poor style. Why?
Hint: What does a reader conclude from the object being there?
Answer:
Because someone reading this code would expect randomInt to be an instance method — the object in front of the dot implies the method uses it, and it does not.
Worse, the object is evaluated and then discarded, so getDeck().randomInt(0, 51) runs getDeck() for nothing. The code says something is happening that is not.
Syntax that compiles can still lie. Writing Deck.randomInt(0, 51) tells the truth: no object is involved. That is the same argument as naming constants in Lesson 3a — make the code say what it means.
Section
Section 13.9
Concept
We could use the Deck class to represent the individual piles. However, our implementation of Deck uses a Card array, and the length of an array can't change. As the game progresses, cards move between piles.
import java.util.ArrayList;
public class Pile {
private ArrayList<Card> cards;
public Pile() {
this.cards = new ArrayList<Card>();
}
}| array | ArrayList | |
|---|---|---|
| length | fixed at creation | grows and shrinks |
| add an element | impossible | add(x) |
| remove an element | impossible | remove(i) |
| package | built into the language | java.util |
collection — An object that contains other objects, providing methods to add and remove elements.
We can solve this problem with an ArrayList, which is in the java.util package. An ArrayList is a collection, which is an object that contains other objects. It provides methods to add and remove elements, and it grows and shrinks automatically.
Notation
ArrayList<Card> is the first time you have written a type inside another type, and the brackets are doing real work.
Annotate
cards.add("hello") is rejected, and cards.get(0) has type Card with no cast needed.new. Both say the same thing.An ArrayList<Card> is as type-safe as a Card[] and, unlike it, can change size. The cost is that it holds objects only — there is no ArrayList<int>, which is a limitation you will meet soon enough.
Worked example
Pile's methods each do one thing: call the matching method on the ArrayList, with a name that suits a pile of cards.
public Card popCard() {
return this.cards.remove(0); // from the top of the pile
}
public void addCard(Card card) {
this.cards.add(card); // to the bottom of the pile
}
public boolean isEmpty() {
return this.cards.isEmpty();
}| Pile method | calls | the pile-language meaning |
|---|---|---|
| popCard() | cards.remove(0) | take the top card |
| addCard(card) | cards.add(card) | put a card on the bottom |
| isEmpty() | cards.isEmpty() | has this player run out? |
remove(0) takes from the front.
Why: popCard removes the Card at the beginning of the ArrayList, which we think of as the top of the pile.
The gap closes itself.
Why: Because we use ArrayList.remove, it automatically shifts the remaining cards to fill the gap.
add appends at the end.
Why: ArrayList provides a method, add, that adds an element to the end of the collection, which we think of as the bottom of the pile.
Name them for the domain.
Why: The ArrayList says index 0; the Pile says the top of the pile.
Verify: Add three cards and pop one; confirm the pile now holds the second and third in that order.
Why: The mapping from ArrayList positions to pile positions is a decision this class makes and hides. Nothing outside Pile needs to know that the top of the pile is index 0 — which is exactly the kind of thing Lesson 11a said encapsulation buys you the freedom to change.
Prediction
An ArrayList has no gaps.
// pile: [A♣, 5♦, K♠]
Card c = pile.popCard();
// pile: ?| before | after remove(0) |
|---|---|
| [A♣, 5♦, K♠] | ? |
Predict first
What is the pile afterwards?
Correct: [5♦, K♠] — the remaining cards shift down to fill the gap
Why: Because we use ArrayList.remove, it automatically shifts the remaining cards to fill the gap — the collection shrinks by one and there is never a null hole. That is precisely what an array cannot do, and the reason Pile uses an ArrayList rather than a Card[].
Concept
So far these methods don't do very much; they just invoke methods on the instance variable, cards. Methods like these are called wrapper methods because they wrap one method with another.
wrapper method — A method that does little more than invoke another method, usually to give it a name or an interface that suits the class.
| what a wrapper buys | even though it adds no logic |
|---|---|
| a name from the problem domain | popCard rather than remove(0) |
| a smaller interface | Pile exposes 4 methods, ArrayList exposes 30 |
| freedom to change | swap ArrayList for something else, unnoticed |
| a place to add logic later | popCard could check for an empty pile |
A wrapper method that adds no behaviour is not a wasted method. It is where the class's vocabulary comes from — and the third row is the one that pays off later, when the storage changes and no caller notices.
Trap
remove() with no argument, or removing from the end.
public Card popCard() {
return this.cards.remove(this.cards.size() - 1); // the BOTTOM
}| method | removes | in pile terms |
|---|---|---|
| remove(0) | the first element | the top of the pile |
| remove(size() - 1) | the last element | the bottom of the pile |
| what War needs | the top | remove(0) |
In War, each player takes the top card and adds won cards to the bottom — that circulation is the game. Taking from the same end you add to turns the pile into a stack and the game into something else.
Take from the front, add to the back.
public Card popCard() {
return this.cards.remove(0); // from the top of the pile
}
public void addCard(Card card) {
this.cards.add(card); // to the bottom of the pile
}| operation | index | pile end |
|---|---|---|
| popCard | 0 | top |
| addCard | the end | bottom |
| net effect | — | cards circulate |
The comments in the book's own code are doing real work here. remove(0) says nothing about piles; from the top of the pile is the sentence that makes the choice checkable by a reader.
Definition probe
Fixed size against changing size.
Sort into buckets
Sort each requirement.
Fill the middle
A Pile holds Cards.
Fill in the blanks
private ArrayList<Card> cards;
public Pile() ()};
}
Why: The angle brackets say what the collection contains, which lets the compiler reject add("hello") and lets get(0) have type Card with no cast. Unlike an array, the constructor takes no size — an ArrayList starts empty and grows as you add.
Explain it to yourself
Pile could just be an ArrayList<Card>.
Discussion prompt
popCard, addCard and isEmpty each call exactly one ArrayList method. What does the Pile class add?
Hint: What does a caller have to know?
Answer:
Vocabulary. A caller writes p1.popCard() rather than p1.cards.remove(0) — the code says what the game does rather than what the data structure does.
And a boundary. The decision that index 0 is the top of the pile lives inside Pile. Change it, or switch to a different collection entirely, and no caller notices.
A wrapper with no logic in it is still doing work — it is where the class's interface is defined. The alternative is every caller knowing that a pile is really an ArrayList, which is exactly the coupling Lesson 11a warned about.
Section
Section 13.10
Concept
Now we can use Deck and Pile to implement the game. Shuffle a deck, cut it in half, and give each player one half.
Deck deck = new Deck();
deck.shuffle();
Pile p1 = new Pile();
p1.addDeck(deck.subdeck(0, 25));
Pile p2 = new Pile();
p2.addDeck(deck.subdeck(26, 51));| line | effect |
|---|---|
| new Deck() | 52 cards, in order |
| deck.shuffle() | random order |
| subdeck(0, 25) | 26 cards for player 1 |
| subdeck(26, 51) | 26 cards for player 2 |
Initially, the deck is divided evenly into two piles, one for each player. Note the ranges: 0 to 25 and 26 to 51 are adjacent and disjoint, which — by Lesson 13a's high - low + 1 — is 26 cards each.
Picture it
addDeck loops through a Deck's cards and adds each to the Pile. Notice what it does not do.
Figure (svg): A Deck and a Pile whose references both point at the same Card objects after addDeck
Notice that it does not remove the cards from the Deck, so the Deck and the Pile share cards. But that won't be a problem because cards are immutable. The same sentence as subdeck, for the same reason.
Worked example
The game itself is a loop that repeats until one of the piles is empty. At each iteration both players draw and the higher rank takes both cards.
while (!p1.isEmpty() && !p2.isEmpty()) {
// pop a card from each pile
Card c1 = p1.popCard();
Card c2 = p2.popCard();
// compare the cards
int diff = c1.getRank() - c2.getRank();
if (diff > 0) {
p1.addCard(c1);
p1.addCard(c2);
} else if (diff < 0) {
p2.addCard(c1);
p2.addCard(c2);
} else {
// it's a tie
}
}| c1 | c2 | diff | who takes both |
|---|---|---|---|
| K♦ (13) | 7♣ (7) | 6 | player 1 |
| 3♠ (3) | J♥ (11) | -8 | player 2 |
| 9♣ (9) | 9♠ (9) | 0 | a tie — exercise |
The condition needs both piles.
Why: while (!p1.isEmpty() && !p2.isEmpty()) — the game ends when either player runs out.
Each player pops a card.
Why: From the top of their pile, which is remove(0).
Compare ranks only.
Why: Whoever has the highest-ranking card, ignoring suit, takes the two cards — so getRank(), not compareTo.
The winner adds both to the bottom.
Why: addCard twice, which is what keeps the cards circulating.
Verify: Play a few rounds by hand and confirm a winner's pile grows by one card per round.
Why: Notice compareTo is deliberately not used here. Card's compareTo ranks suit first, and War ignores suit entirely — so the game does its own comparison with getRank(). That is the arbitrariness Lesson 12a warned about, arriving on schedule: the class's ordering is not every game's ordering.
Prediction
Both conditions are checked each round.
while (!p1.isEmpty() && !p2.isEmpty()) {
...
}| p1 | p2 | loop continues? |
|---|---|---|
| 26 cards | 26 cards | yes |
| 52 cards | 0 cards | no |
Predict first
The loop ends. What do you know?
Correct: At least one pile is empty, and one test tells you which
Why: The && means the loop continues only while both piles have cards, so it stops as soon as either one empties. Afterwards if (p2.isEmpty()) distinguishes the two cases — and since a round moves cards only between the piles, exactly one of them is empty.
Concept
After the while loop ends, we display the winner based on which pile is not empty.
if (p2.isEmpty()) {
System.out.println("Player 1 wins!");
} else {
System.out.println("Player 2 wins!");
}| loop ended because | so | winner |
|---|---|---|
| p2 is empty | player 2 ran out | player 1 |
| p1 is empty | player 1 ran out | player 2 |
The loop condition and the winner test are two halves of one fact. The loop stops when either pile empties, so afterwards exactly one test is needed to find out which — the same reasoning as returning -1 after a search loop in Lesson 12b.
Trap
Card's compareTo ranks by suit first — War does not.
int diff = c1.compareTo(c2); // wrong for this game
// 2 of Spades vs King of Clubs:
// compareTo says the 2 wins (Spades beats Clubs)
// War says the King wins (rank only)| c1 | c2 | compareTo says | War says |
|---|---|---|---|
| 2♠ | K♣ | c1 is higher | c2 wins |
| A♠ | K♠ | c2 is higher | c2 wins — agrees by luck |
The second row agrees, which is the dangerous part: the bug shows up only on cards of different suits, so casual testing can miss it entirely.
Compare the ranks, because that is this game's rule.
int diff = c1.getRank() - c2.getRank();
if (diff > 0) { /* player 1 takes both */ }
else if (diff < 0) { /* player 2 takes both */ }
else { /* it's a tie */ }| diff | meaning |
|---|---|
| positive | c1 outranks c2 |
| negative | c2 outranks c1 |
| zero | same rank — a tie, whatever the suits |
Whoever has the highest-ranking card, ignoring suit, takes the two cards. Lesson 12a said the ordering in compareTo is arbitrary and might be different for different games — this is the game where it is different, and the fix is for the game to compare its own way.
Prediction
Card has a compareTo, and the game does not use it.
int diff = c1.getRank() - c2.getRank();| compareTo | War | |
|---|---|---|
| compares suit first | yes | no |
| ignores suit | no | yes |
Predict first
What is the reason?
Correct: War ignores suit, and Card's compareTo ranks suit before rank
Why: Lesson 12a chose to compare suit first, and noted the choice is arbitrary and might be different for different games. War is such a game: the 2 of Spades loses to the King of Clubs, which is the opposite of what compareTo would say.
Definition probe
Three classes, three responsibilities.
Sort into buckets
Sort each responsibility.
Real world
One of the exercises asks you to implement the else block when there's a tie.
Discussion prompt
On a tie, each player draws four more cards and whoever has the higher fourth card takes everything — and another tie repeats the process. What makes that harder than the other two branches?
Hint: What if a player has fewer than four cards left?
Answer:
A player might not have four cards. The rules assume a supply the piles cannot guarantee, so the code needs a decision the rules do not make.
And the tie can repeat, so the eight cards in play have to be accumulated somewhere across an unknown number of rounds before being awarded.
The awkward cases are where the specification runs out, and no amount of reading the rules resolves them. That is why Think Java leaves it as an exercise — it is the part where you have to decide, and decide the same way every time.
Section
Chapters 12-13 together
Concept
War is about thirty lines because Card, Deck and Pile each absorbed something. The game loop reads like the rules because everything else is elsewhere.
Deck deck = new Deck(); // Deck knows how to build 52 cards
deck.shuffle(); // Deck knows how to randomise
p1.addDeck(deck.subdeck(0, 25)); // Deck knows how to split
Card c1 = p1.popCard(); // Pile knows what "the top" means
c1.getRank(); // Card knows the encoding| class | hides | so the game never mentions |
|---|---|---|
| Card | the integer encoding | that a King is 13 |
| Deck | the array and the loops | nested suit and rank loops |
| Pile | the ArrayList | index 0 or remove |
Every line of the game says what the game does. Nothing in it says how a deck is stored or how a card is encoded — which is the return on all the design work of the last two chapters.
Picture it
Deck holds an array of Cards; Pile holds an ArrayList of Cards; and Pile can be filled from a Deck.
Figure (svg): A UML diagram showing Deck and Pile both containing Card objects and Pile taking a Deck
Both Deck and Pile contain Cards, and neither contains the other. Pile takes a Deck as a parameter and reads its cards — a much looser relationship than containment, and one that Chapter 14 will replace with something stronger.
Worked example
Deck and Pile do nearly the same job with different storage, and the difference is exactly one requirement.
public class Deck {
private Card[] cards; // fixed size
}
public class Pile {
private ArrayList<Card> cards; // changing size
}| Deck | Pile | |
|---|---|---|
| storage | Card[] | ArrayList<Card> |
| size | fixed at creation | changes every round |
| needs add/remove? | no | yes |
| needs sorting? | yes | no |
Ask what changes.
Why: A deck has 52 cards throughout; a pile gains and loses cards constantly.
Match the structure to that.
Why: The length of an array can't change, which settles it for Pile.
Notice what Deck keeps.
Why: Arrays index directly, which is what subdeck and the sorts need.
Notice neither is wrong.
Why: Two classes, two requirements, two structures.
Verify: Try to write addCard for Deck and see where it fails: there is no way to lengthen Card[].
Why: A data structure is chosen by what the program needs to do to it, not by which is newer or more capable. Using an ArrayList for Deck would work; using an array for Pile would not.
Definition probe
Each fact belongs in exactly one place.
Sort into buckets
Sort each piece of knowledge.
Concept
Deck and Pile are suspiciously similar — both are collections of cards with overlapping methods. That similarity is the opening of the next chapter.
| Deck | Pile | shared? | |
|---|---|---|---|
| holds cards | yes | yes | yes |
| yes | would want it | yes | |
| shuffle | yes | would want it | yes |
| a name | no | no | — |
| sorting | yes | no | no |
Two classes doing almost the same thing is the signal for inheritance, which Chapter 14 introduces with a CardCollection that both can extend. The duplication you can see here is what motivates it — which is why the book builds both classes first.
Trap
Reaching past the class's methods.
// if Pile exposed its ArrayList:
Card c1 = p1.getCards().remove(0);
p1.getCards().add(c2);| problem | consequence |
|---|---|
| the game knows it is an ArrayList | changing the storage breaks the game |
| the game knows 0 is the top | the convention is now in two places |
| Pile's methods are bypassed | any check they do is skipped |
It works. And now the decision that index 0 is the top of the pile lives in the game loop as well as in Pile, so changing it means finding every copy.
Ask the class, in the class's own vocabulary.
Card c1 = p1.popCard();
p1.addCard(c2);| benefit | detail |
|---|---|
| the game reads like the rules | pop a card, add a card |
| the storage is Pile's business | swap it freely |
| one place holds the convention | inside Pile |
The wrapper methods exist for exactly this. They looked like they added nothing; what they added was the boundary that keeps the game loop free of data-structure details.
Prediction
Deck stores a Card array.
public class Deck {
private Card[] cards;
public void addCard(Card card) {
// ?
}
}| array | ArrayList | |
|---|---|---|
| length after creation | fixed | changes |
Predict first
What is the obstacle?
Correct: An array's length is fixed, so there is no slot to add to
Why: The length of an array can't change, so adding would mean allocating a bigger array and copying — which is what an ArrayList does for you. That single limitation is the whole reason Pile exists as a separate class rather than reusing Deck.
Matching
Each absorbs one decision.
Match the pairs
Why: Each class hides one kind of decision, which is why the game loop reads like the rules of War and mentions no arrays, no indexes and no rank encodings. That separation is the return on two chapters of design work.
Counterexample
An ArrayList can do everything an array can.
Discussion prompt
If ArrayList grows and shrinks and Card[] does not, why keep Deck's array at all?
Hint: What does subdeck do with indexes?
Answer:
It could. An ArrayList<Card> would serve Deck perfectly well, with get(i) and set(i, c) in place of the bracket syntax.
The array is simpler for what Deck does: fixed size, direct indexing, and sorting algorithms written in terms of positions. Nothing Deck does needs the size to change.
Choose the simplest structure that meets the requirement, not the most capable one. And the similarity you noticed is real — it is exactly what Chapter 14 addresses by giving both classes a common parent.
Comparison
Fill the blanks.
Comparison matrix
| static method | instance method | |
|---|---|---|
| invoked on | the class — Deck.randomInt(...) | an object — deck.print() |
| can use this | no | yes |
| can read this.cards | no | yes |
| UML notation | underlined | not underlined |
| the deciding question | does it touch an instance variable? | same question, other answer |
One question decides it: does the method read or write an instance variable? If not, it does not need an object and should not require one.
Pattern
A wrapper class: hide a general-purpose data structure behind names from your own problem.
public class Pile {
private ArrayList<Card> cards; // 1. the storage, private
public Card popCard() { // 2. wrapper methods named
return this.cards.remove(0); // for the problem domain
}
public void addCard(Card card) {
this.cards.add(card);
}
public boolean isEmpty() {
return this.cards.isEmpty();
}
}
// 3. callers speak the domain, not the data structure
Card c1 = p1.popCard();| decision | where it lives | who else knows |
|---|---|---|
| index 0 is the top of the pile | inside Pile | nobody |
| the storage is an ArrayList | inside Pile | nobody |
| a King is rank 13 | inside Card | nobody |
| War ignores suit | the game loop | nobody else needs to |
Check
Work it out before you click.
public Deck mergeSort() {
// if the deck has 0 or 1 cards, return it
// otherwise divide, sort each half, merge
}| deck size | handled how |
|---|---|
| 0 | returned |
| 1 | returned |
Check your understanding
Why are both zero and one treated as base cases?
Answer: A
Why: One card cannot be out of order, and neither can zero cards, so both are sorted by definition. Think Java calls the zero case convenient rather than necessary — careful splitting never produces an empty subdeck, but splitting arithmetic is exactly where off-by-one errors live, and without the guard such a slip recurses forever.
subdeck(3, 3) produces one.Check
Work it out before you click.
private static Deck merge(Deck d1, Deck d2) {
return this.cards;
}| merge is | this requires |
|---|---|
| static | an object |
Check your understanding
What error does this produce?
Answer: A
Why: A static method is not invoked on an object, so there is no object for this to refer to. The fix is to use the parameters — d1.cards is legal even though cards is private, because private means within this class, not within this object.
Deck.print().d1.cards, but not this — there is still no object.Check
Work it out before you click.
// pile holds [A♣, 5♦, K♠]
Card c = pile.popCard();
pile.addCard(c);| operation | index used |
|---|---|
| popCard | remove(0) |
| addCard | add — appends |
Check your understanding
What does the pile hold afterwards?
Answer: A
Why: popCard removes from index 0 and the remaining elements shift down to fill the gap; addCard appends at the end. So the card that was on top is now on the bottom — which is exactly the circulation War needs, and something a fixed-length array could not do.
Real world
Pile wraps ArrayList and adds no behaviour. That pattern is everywhere in working software.
Discussion prompt
Where else does code wrap a general-purpose thing in a class named for the problem — and what goes wrong when it does not?
Hint: A list of strings called what, exactly?
Answer:
Almost every well-organised program does it: an Inventory wrapping a Map, an Order wrapping a list of items, a Playlist wrapping a queue. The wrapper supplies the vocabulary.
Without it, the data structure leaks everywhere. Every caller has to know that index 0 means the top, and that convention ends up copied into a dozen places — where one of the copies is eventually wrong.
The wrapper is where a decision goes to be made once. That is the same argument as private instance variables in Lesson 11a and named constants in Lesson 3a, at a larger scale — and it is why methods that don't do very much are still worth writing.
Commit first
Commit to an answer and to your confidence.
Predict first
Why is merge static while subdeck is an instance method?
Correct: merge gets both decks as parameters and touches no instance variable; subdeck reads this.cards
Why: Think Java states the rule outright: the helper methods randomInt and merge are static, because they do not read or write any instance variables. All other methods are instance methods, because they access the instance variable, cards. Everything merge needs arrives as a parameter, so it needs no object — and requiring one would mislead the reader. Note that being static does not stop it reading d1.cards: private means within this class, not within this object, which is why merge can see both decks' arrays.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate has hit non-static variable this cannot be referenced from a static context and does not know what it means. Explain the message and give them two ways to fix it.
Hint: Translate static context into plain English first.
Answer:
Static context means there is no object here. A static method belongs to the class, and it can be called when no objects exist at all — so there is nothing for this to point at.
Two fixes. Either take what you need as a parameter, so the method is given an object to work on. Or drop the static keyword, if the method really does operate on one particular object.
Which one depends on a single question: does the method need this object specifically, or just some object? Think Java admits these messages are confusing and frustrating for beginners — the translation is what makes them stop being so.
Exit ticket
One question before you close the deck.
Predict first
Why does Pile use an ArrayList while Deck uses an array?
Correct: A pile grows and shrinks during the game, and an array's length cannot change
Why: Think Java's reasoning is exactly this: our implementation of Deck uses a Card array, and the length of an array can't change. As the game progresses, we need to be able to add and remove cards from the piles. A deck is always 52 cards, so an array is simpler and its direct indexing is what subdeck and the sorts need. The structure follows the requirement — neither choice is better in general.
Connect it up
One page, from memory.
Draw it
Draw mergeSort's call tree for a deck of 8, marking the base-case leaves and labelling the depth as log₂ n. Beside it, write both non-static-context error messages with a one-line translation of each. Then draw a Pile object holding an ArrayList and write its three wrapper methods, marking which end of the list is the top of the pile. Finish by writing the one line of War that would be wrong if it used compareTo, and why.
Recap
Four sections that finish the sorting, name the static confusion, and introduce the first collection class.
| if you remember one thing | it is this |
|---|---|
| about recursion | check the base cases, then stop tracing |
| about static | static context means no object here |
| about data structures | choose by what has to change |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.