Recursive Merge Sort, Static Context, and Piles of Cards

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

What this lesson covers

The lesson, slide by slide

1. Recursive Merge Sort, Static Context, and Piles of Cards

Title

Think Java 2e · Chapter 13 · Objects of Arrays

Sections 13.7-13.10 · pp. 224-230

2. What you will be able to do

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.

3. Retrieve before you read

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.

4. One call changes

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.

5. Adding recursion

Section

Section 13.7

6. Two base cases, one for safety

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 sizewhat happenswhy
0 cardsreturn itno cards, so they cannot be out of order
1 cardreturn itone card cannot be out of order
2 or moredivide, sort each, mergethe 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.

7. Why handle zero cards as well?

Notation

One card is enough to make the recursion terminate. The book adds a second base case anyway.

Annotate

  • **The word is convenient, not necessary.** With careful splitting you never produce an empty subdeck.
  • But the splitting arithmetic is where off-by-one errors live — Lesson 13a's high - low + 1 warning. An empty subdeck is what a small mistake produces.
  • Without the zero case, an empty subdeck would recurse forever or index past the end of an array.
  • With it, an empty subdeck is simply sorted, and the algorithm survives an arithmetic slip that would otherwise hang.
  • The same reasoning as the null placeholder in Lesson 12a: handle the degenerate case even if it should never arise, because should never is not cannot.

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.

8. The leap of faith, deliberately

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
callyou assumewhy that is fair
selectionSort(half)it returns a sorted halfyou debugged it
mergeSort(half)it returns a sorted halfsame claim, same method
merge(a, b)it returns them combined in orderyou 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.

9. What happens with no base case?

Prediction

The method divides and recurses, forever.

public Deck mergeSort() {
    // divide into two subdecks
    // sort each with mergeSort
    // merge and return
}
depthdeck size
052
61
71 — and still recursing

Predict first

What happens?

  • It recurses forever and throws a StackOverflowError
  • It sorts correctly but slowly
  • It does not compile
  • It returns an empty deck

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.

10. The call tree

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.

11. A recursive call that is not smaller

Trap

The 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());
}
sizeabshrinking?
40..2 (3 cards)2..3 (2 cards)yes, but a card is duplicated
20..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.

The fix

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());
}
sizeabboth smaller?
40..1 (2)2..3 (2)yes
20..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.

12. Base case or recursive case?

Definition probe

Which deck sizes are handled directly?

Sort into buckets

Sort each size.

base case — return it
0 cards; 1 card
recursive case — split
2 cards; 52 cards
base
There is no way for the cards to be out of order, so the deck is already sorted and can be returned unchanged.
rec
Two or more cards can be out of order, so the deck must be split, each half sorted, and the halves merged.

13. Complete the recursive step

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.

14. Why does the leap of faith work here?

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.

15. Static context

Section

Section 13.8

16. A UML diagram, and which methods are static

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

notationmeans
-private
+public
underlinedstatic
listed above the lineattributes — 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.

17. Two error messages, spelled out

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.
  • **By static context the compiler means you are trying to invoke a method in a context that requires a static method.**
  • The second error is using 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.
  • For beginners, error messages about non-static context can be confusing and frustrating. We hope this section helps. The book says so outright — you are not the first.

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.

18. Getting the invocation right

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!
calllegal?why
deck.print()yesdeck is an object, print is an instance method
Deck.print()noDeck is a class; print needs an object
Deck.randomInt(0, 51)yesrandomInt is static
deck.randomInt(0, 51)legal, poor stylesee 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.

19. What does Deck.print() produce?

Prediction

print is an instance method.

Deck deck = new Deck();
Deck.print();
print needsDeck is
an object, for this.cardsa class

Predict first

What happens?

  • A compile error: non-static method print() cannot be referenced from a static context
  • It prints an empty deck
  • It prints the deck stored in the variable deck
  • It compiles but does nothing

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.

20. The rule, in one question

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 iscall it on
yesan instance methodan object
noa static methodthe class
examples that doprint, shuffle, subdeck, swapCardsdeck
examples that do notrandomInt, mergeDeck

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.

21. Using this in a static method

Trap

The 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 methodstatic method
invoked onan objectthe class
has this?yesno
can read cards?yesno
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.

The fix

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
needfix
a deck to work ontake it as a parameter
this deck specificallymake the method non-static
nothing from any objectleave 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.

22. Static or instance?

Definition probe

The question is whether it touches this.cards.

Sort into buckets

Sort each Deck method.

static
randomInt(low, high); merge(d1, d2)
instance
subdeck(low, high); shuffle()
st
It reads and writes no instance variable — everything it needs arrives as a parameter — so it belongs to the class rather than to any deck.
in
It reads or writes this.cards, so it needs a particular deck to operate on.

23. Match the error to its cause

Matching

Two messages, two mistakes.

Match the pairs

  • a. Non-static method print() cannot be referenced from a static context
  • b. Non-static variable this cannot be referenced from a static context
  • c. underlined in UML
  • d. a minus sign in UML
  • r1. calling an instance method on the class
  • r2. using this inside a static method
  • r3. static
  • r4. private

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.

24. Why is deck.randomInt(0, 51) bad style?

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.

25. ArrayList and the Pile class

Section

Section 13.9

26. A collection that grows and shrinks

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>();
    }
}
arrayArrayList
lengthfixed at creationgrows and shrinks
add an elementimpossibleadd(x)
remove an elementimpossibleremove(i)
packagebuilt into the languagejava.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.

27. The angle brackets

Notation

ArrayList<Card> is the first time you have written a type inside another type, and the brackets are doing real work.

Annotate

  • When you declare an ArrayList, you specify the type it contains in angle brackets.
  • This declaration says that cards is not just an ArrayList; it's an ArrayList of Card objects.
  • The compiler uses it. cards.add("hello") is rejected, and cards.get(0) has type Card with no cast needed.
  • The constructor initialises this.cards with an empty ArrayList — note that unlike an array, you do not give it a size.
  • The brackets appear twice, once in the declaration and once in the 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.

28. Three wrapper methods

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 methodcallsthe 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.

29. What does remove(0) do to the rest?

Prediction

An ArrayList has no gaps.

// pile: [A♣, 5♦, K♠]
Card c = pile.popCard();
// pile: ?
beforeafter remove(0)
[A♣, 5♦, K♠]?

Predict first

What is the pile afterwards?

  • [5♦, K♠] — the remaining cards shift down to fill the gap
  • [null, 5♦, K♠]
  • [A♣, 5♦, K♠] — remove returns a copy
  • [5♦, K♠, null]

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[].

30. Wrapper methods

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 buyseven though it adds no logic
a name from the problem domainpopCard rather than remove(0)
a smaller interfacePile exposes 4 methods, ArrayList exposes 30
freedom to changeswap ArrayList for something else, unnoticed
a place to add logic laterpopCard 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.

31. Removing from the wrong end

Trap

The trap

remove() with no argument, or removing from the end.

public Card popCard() {
    return this.cards.remove(this.cards.size() - 1);   // the BOTTOM
}
methodremovesin pile terms
remove(0)the first elementthe top of the pile
remove(size() - 1)the last elementthe bottom of the pile
what War needsthe topremove(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.

The fix

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
}
operationindexpile end
popCard0top
addCardthe endbottom
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.

32. Array or ArrayList?

Definition probe

Fixed size against changing size.

Sort into buckets

Sort each requirement.

Card[]
exactly 52 cards, forever; a fixed-size subdeck for merging
ArrayList<Card>
a pile that grows and shrinks during play; cards added and removed one at a time
arr
The size is known and never changes, so an array is simpler and the fixed length is no obstacle.
list
The number of elements changes as the program runs, which an array cannot do — its length is fixed at creation.

33. Declare the ArrayList

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.

34. Why wrap ArrayList at all?

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.

35. Playing War

Section

Section 13.10

36. Dealing the piles

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));
lineeffect
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.

37. addDeck shares, it does not move

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.

38. The game loop

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
    }
}
c1c2diffwho takes both
K♦ (13)7♣ (7)6player 1
3♠ (3)J♥ (11)-8player 2
9♣ (9)9♠ (9)0a 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.

39. When does the loop stop?

Prediction

Both conditions are checked each round.

while (!p1.isEmpty() && !p2.isEmpty()) {
    ...
}
p1p2loop continues?
26 cards26 cardsyes
52 cards0 cardsno

Predict first

The loop ends. What do you know?

  • At least one pile is empty, and one test tells you which
  • Both piles are empty
  • The piles have the same number of cards
  • Player 1 has won

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.

40. Ending the game

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 becausesowinner
p2 is emptyplayer 2 ran outplayer 1
p1 is emptyplayer 1 ran outplayer 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.

41. Using compareTo to decide a round

Trap

The 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)
c1c2compareTo saysWar says
2♠K♣c1 is higherc2 wins
A♠K♠c2 is higherc2 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.

The fix

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 */ }
diffmeaning
positivec1 outranks c2
negativec2 outranks c1
zerosame 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.

42. Why getRank rather than compareTo?

Prediction

Card has a compareTo, and the game does not use it.

int diff = c1.getRank() - c2.getRank();
compareToWar
compares suit firstyesno
ignores suitnoyes

Predict first

What is the reason?

  • War ignores suit, and Card's compareTo ranks suit before rank
  • compareTo does not work on Cards
  • getRank is faster
  • compareTo is private

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.

43. Which class does each job?

Definition probe

Three classes, three responsibilities.

Sort into buckets

Sort each responsibility.

Card
knowing a card's rank and suit
Deck
shuffling and dealing
Pile
a player's changing collection of cards
the game loop
deciding who wins a round
card
An immutable value: a rank, a suit, and how to display and compare them.
deck
A fixed-size array of 52 cards, with the operations that rearrange it.
pile
A collection that grows and shrinks, because a player's holding changes every round.
game
The rules — including that War ignores suit, which is not something any of the classes should assume.

44. Why is a tie awkward to implement?

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.

45. What the three classes bought

Section

Chapters 12-13 together

46. Each class holds one decision

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
classhidesso the game never mentions
Cardthe integer encodingthat a King is 13
Deckthe array and the loopsnested suit and rank loops
Pilethe ArrayListindex 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.

47. Three classes and their relationships

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.

48. Two collections, two reasons

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
}
DeckPile
storageCard[]ArrayList<Card>
sizefixed at creationchanges every round
needs add/remove?noyes
needs sorting?yesno

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.

49. Which class should know what?

Definition probe

Each fact belongs in exactly one place.

Sort into buckets

Sort each piece of knowledge.

Card
that a King is rank 13
Deck
that a full deck has 52 cards
Pile
that index 0 is the top of the pile
the game
that War ignores suit
card
The encoding is Card's own business — the RANKS array and compareTo are the only places that need it.
deck
The size and the nested loops that build a full deck belong to the constructor.
pile
Which end of the ArrayList is the top is a decision Pile makes and hides behind popCard and addCard.
game
A rule about how to compare cards in this particular game, which is why the loop uses getRank rather than compareTo.

50. Where Chapter 14 goes next

Concept

Deck and Pile are suspiciously similar — both are collections of cards with overlapping methods. That similarity is the opening of the next chapter.

DeckPileshared?
holds cardsyesyesyes
printyeswould want ityes
shuffleyeswould want ityes
a namenono—
sortingyesnono

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.

51. Letting the game know how a pile is stored

Trap

The trap

Reaching past the class's methods.

// if Pile exposed its ArrayList:
Card c1 = p1.getCards().remove(0);
p1.getCards().add(c2);
problemconsequence
the game knows it is an ArrayListchanging the storage breaks the game
the game knows 0 is the topthe convention is now in two places
Pile's methods are bypassedany 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.

The fix

Ask the class, in the class's own vocabulary.

Card c1 = p1.popCard();
p1.addCard(c2);
benefitdetail
the game reads like the rulespop a card, add a card
the storage is Pile's businessswap it freely
one place holds the conventioninside 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.

52. Why can't Deck use addCard?

Prediction

Deck stores a Card array.

public class Deck {
    private Card[] cards;

    public void addCard(Card card) {
        // ?
    }
}
arrayArrayList
length after creationfixedchanges

Predict first

What is the obstacle?

  • An array's length is fixed, so there is no slot to add to
  • Card is immutable
  • cards is private
  • Deck has no constructor for it

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.

53. Match the class to what it hides

Matching

Each absorbs one decision.

Match the pairs

  • a. Card
  • b. Deck
  • c. Pile
  • d. the game loop
  • r1. the integer encoding of ranks and suits
  • r2. the array and the nested loops
  • r3. the ArrayList and which end is the top
  • r4. the rules of War

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.

54. Could Pile replace Deck entirely?

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.

55. Static against instance, and array against ArrayList

Comparison

Fill the blanks.

Comparison matrix

static methodinstance method
invoked onthe class — Deck.randomInt(...)an object — deck.print()
can use thisnoyes
can read this.cardsnoyes
UML notationunderlinednot underlined
the deciding questiondoes 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.

56. The pattern to carry away

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();
decisionwhere it liveswho else knows
index 0 is the top of the pileinside Pilenobody
the storage is an ArrayListinside Pilenobody
a King is rank 13inside Cardnobody
War ignores suitthe game loopnobody else needs to

57. Check: base cases

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 sizehandled how
0returned
1returned

Check your understanding

Why are both zero and one treated as base cases?

  • A. Neither can be out of order, so both are already sorted — and the zero case guards against a splitting mistake (correct)
  • B. Java cannot create a deck of one card
  • C. Because merge cannot take an empty deck
  • D. To make the method faster

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.

Why B tempts people
A one-card deck is perfectly constructible — subdeck(3, 3) produces one.
Why C tempts people
A correct merge handles an empty input fine; the base case is about terminating the recursion.
Why D tempts people
Speed is a side effect; the base case is what makes the method terminate at all.

58. Check: static context

Check

Work it out before you click.

private static Deck merge(Deck d1, Deck d2) {
    return this.cards;
}
merge isthis requires
statican object

Check your understanding

What error does this produce?

  • A. Non-static variable this cannot be referenced from a static context (correct)
  • B. Non-static method merge() cannot be referenced from a static context
  • C. cards has private access in Deck
  • D. It compiles — merge is inside the Deck class

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.

Why B tempts people
That is the error for calling an instance method on the class, like Deck.print().
Why C tempts people
Access is not the problem; a method in Deck may read any Deck's private variables.
Why D tempts people
Being inside the class permits reading d1.cards, but not this — there is still no object.

59. Check: ArrayList

Check

Work it out before you click.

// pile holds [A♣, 5♦, K♠]
Card c = pile.popCard();
pile.addCard(c);
operationindex used
popCardremove(0)
addCardadd — appends

Check your understanding

What does the pile hold afterwards?

  • A. [5♦, K♠, A♣] — the card moved from the top to the bottom (correct)
  • B. [A♣, 5♦, K♠] — unchanged
  • C. [A♣, A♣, 5♦, K♠]
  • D. [5♦, K♠] — the card was lost

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.

Why B tempts people
The card genuinely moves; remove and add operate on different ends.
Why C tempts people
remove takes the card out, so it is not duplicated by adding it back.
Why D tempts people
popCard returns the card, and addCard puts it back — nothing is lost.

60. Wrapping a general tool in a specific name

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.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

Why is merge static while subdeck is an instance method?

  • merge gets both decks as parameters and touches no instance variable; subdeck reads this.cards
  • merge is private and subdeck is public
  • merge returns a Deck and subdeck does not
  • merge is recursive and subdeck is not

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

Why does Pile use an ArrayList while Deck uses an array?

  • A pile grows and shrinks during the game, and an array's length cannot change
  • An ArrayList is always better than an array
  • Cards in a pile are mutable
  • A pile can hold more than 52 cards

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.

64. Draw the whole lesson

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.

65. Recap

Recap

Four sections that finish the sorting, name the static confusion, and introduce the first collection class.

if you remember one thingit is this
about recursioncheck the base cases, then stop tracing
about staticstatic context means no object here
about data structureschoose by what has to change

Sources

  1. 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
  2. The Java Tutorials — Understanding Class Members (static)
  3. Think Java 2e — free online edition and source code

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

Book on Wyzant · Text (657) 465-8108