The Crazy Eights rules at last, in a Player class that decides which card to play and an Eights class that holds the state of the game — assembled by bottom-up design, the counterpart to Chapter 13's top-down. Then the two relationships every object-oriented design is made of: HAS-A and IS-A. Follows Think Java 2e, Chapter 14 (Extending Classes), Sections 14.4-14.6, pp. 240-247, cross-referenced against The Java Tutorials — Overriding and Hiding Methods.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 14 · Extending Classes
Sections 14.4-14.6 · pp. 240-247
Objectives
This lesson follows Think Java 2e, Chapter 14 (Extending Classes), Sections 14.4-14.6, pp. 240-247. Everything on these slides can be checked against those pages.
1. Separate game rules from reusable card classes, and say why that separation is worth keeping.
2. Write a strategy method that delegates to helper methods for its two cases.
3. Explain bottom-up design and contrast it with top-down design.
4. Trace a game loop that alternates between two players until one hand is empty.
5. Distinguish composition (HAS-A) from inheritance (IS-A).
6. Read and draw a UML class diagram using both kinds of arrow.
Warm-up
Two ideas the chapter is about to use by name.
Discussion prompt
From Lesson 13a: what is top-down design, and what does pseudocode give you? And from Lesson 14a: what is the difference between saying a Player is a Hand and a Player has a Hand?
Hint: One writes the outline first. The other is the whole basis of the next section.
Answer:
Top-down design means writing pseudocode first and then writing the helper methods to make it work. And is a means inheritance — extends, with substitution — while has a means composition: an instance variable holding one.
A Player has a Hand, which is why Lesson 14a rejected Player extends Hand. This lesson writes that class, and Section 14.6 finally gives both relationships their proper names.
Concept
The Deck and Hand classes we have defined so far could be used for any card game; we have not yet implemented any of the rules specific to Crazy Eights. But now it's time to implement the rules.
Figure (svg): Two panels separating the reusable card classes from the two game-specific classes
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 14 (Extending Classes), Sections 14.4-14.6, pp. 240-247 — Section 14.4 opens on printed page 240.
Section
Section 14.4
Concept
We'll use two classes: Player, which encapsulates player strategy, and Eights, which creates and maintains the state of the game.
public class Player {
private String name;
private Hand hand;
public Player(String name) {
this.name = name;
this.hand = new Hand(name);
}
}| attribute | type | relationship |
|---|---|---|
| name | String | a Player has a name |
| hand | Hand | a Player has a Hand |
A Player has two private attributes: a name and a hand. The constructor takes the player's name as a string and saves it in an instance variable. In this example, we have to use this to distinguish between the instance variable and the parameter with the same name. Lesson 14a dropped this everywhere it was optional — this is a place it is not.
Picture it
Both relationships in one picture. The arrows point differently on purpose.
Figure (svg): A UML diagram showing Player composed of a Hand and Hand inheriting from CardCollection
Hand IS-A CardCollection; Player HAS-A Hand. A Player cannot be dealt cards directly and never should be — every card operation goes through hand, which is exactly the boundary composition draws.
Worked example
The primary method that Player provides is play, which decides which card to discard during each turn. It is five lines and neither helper exists yet.
public Card play(Eights eights, Card prev) {
Card card = searchForMatch(prev);
if (card == null) {
card = drawForMatch(eights, prev);
}
return card;
}| parameter | is |
|---|---|
| eights | the object holding the state of the game |
| prev | the card on top of the discard pile |
| the return value | the card this player chose to play |
Look in your hand first.
Why: searchForMatch(prev) returns a matching card, or null.
null means nothing matched.
Why: If there are no cards that match, it returns null.
Then draw until you get one.
Why: drawForMatch(eights, prev) — the rule that says you must keep drawing.
Neither helper is written yet.
Why: play invokes two helper methods: searchForMatch and drawForMatch. Since we have not written them yet, this is an example of top-down design.
Verify: Read play and check that it states the rule in five lines with no card handling in it.
Why: The two branches of the rule become the two helper methods, which is Lesson 13a's process exactly: the outline names the pieces. Note that null is being used as no card found — a sentinel, like -1 for a failed search in Lesson 12b.
Prediction
The loop finishes without finding anything.
for (int i = 0; i < hand.size(); i++) {
...
}
return null;| outcome | return |
|---|---|
| a match | the card |
| no match | ? |
Predict first
What comes back?
Correct: null — and play checks for it to decide whether to draw
Why: null is the sentinel for no card found, exactly as -1 was for a failed search in Lesson 12b — and the return sits after the loop, since only a finished loop proves no card matched. play tests for it with if (card == null).
Concept
searchForMatch looks in the player's hand for a card that matches the previously played card. drawForMatch handles the other case.
public Card searchForMatch(Card prev) {
for (int i = 0; i < hand.size(); i++) {
Card card = hand.getCard(i);
if (cardMatches(card, prev)) {
return hand.popCard(i);
}
}
return null;
}| outcome | returns | side effect |
|---|---|---|
| a match is found | the card, removed from the hand | the hand shrinks |
| no match | null | none |
| which card, if several match | the first one found | the strategy is simple on purpose |
The strategy is pretty simple: the for loop searches for the first card that's legal to play and returns it. Exercise 14.1 asks you to do better — playing an eight only when you must, or the highest-ranking card first — by writing a subclass that overrides play.
Trap
getCard reads; popCard removes.
if (cardMatches(card, prev)) {
return hand.getCard(i); // the card is STILL in the hand
}| method | returns the card? | removes it? |
|---|---|---|
| hand.getCard(i) | yes | no |
| hand.popCard(i) | yes | yes |
The card gets added to the discard pile and stays in the player's hand, so the game now has 53 cards and a player who can never run out. Cards stop being conserved, and the game never ends.
popCard, so the card actually leaves the hand.
if (cardMatches(card, prev)) {
return hand.popCard(i);
}| step | hand size |
|---|---|
| before | 5 |
| popCard(i) | 4 |
| the card is added to the discard pile | conserved |
Note the loop reads with getCard and then removes with popCard — read first, remove only once you have decided. Removing while searching would shift the remaining cards under the loop's own index, which is a separate bug of the same family.
Definition probe
The rules live in exactly one place.
Sort into buckets
Sort each piece of knowledge.
Fill the middle
Read each card, and remove the one you play.
Fill in the blanks
Card card = hand.getCard(i);
if (cardMatches(card, prev)) popCard}(i);
}
Why: getCard reads without modifying, so the loop can inspect each card in turn; popCard removes the one actually played. Using getCard for both would leave the card in the hand as well as on the discard pile, and the game would never end.
Socratic
It only needs one card from it.
Discussion prompt
play(Eights eights, Card prev) passes the whole game object. Why does the player need it — and what does that say about who owns the draw pile?
Hint: Where does a drawn card come from?
Answer:
Because drawing a card means taking one from the draw pile, and the draw pile belongs to the Eights object, not to any player.
So the player asks the game: eights.drawCard(). That method also handles reshuffling when the pile runs out — a rule the player should not have to know.
The state belongs to the game; the strategy belongs to the player. Passing this into play is how the two talk without either owning the other — and it is why takeTurn calls player.play(this, prev).
Section
Section 14.4
Concept
When players don't have a matching card or an eight, they must draw new cards until they get one. That rule translates into a loop with no obvious end.
public Card drawForMatch(Eights eights, Card prev) {
while (true) {
Card card = eights.drawCard();
System.out.println(name + " draws " + card);
if (cardMatches(card, prev)) {
return card;
}
hand.addCard(card);
}
}| drawn card | matches? | what happens |
|---|---|---|
| 3 of Clubs | no | added to the hand, draw again |
| Queen of Hearts | no | added to the hand, draw again |
| 8 of Spades | yes — wild | returned, and played |
The while loop runs until it finds a match (we'll assume for now that it always finds one). That parenthesis is the book being honest: while (true) with a return inside is correct only if a match eventually turns up.
Notation
The loop has no condition. Everything that ends it is inside the body.
Annotate
while (true) never ends on its own — the return is the only exit, which Lesson 6a called a loop that must be read carefully.A while (true) is not automatically bad, but it is a promise you are making. Here the promise is that a match exists — reasonable, since eights are wild and there are four of them.
Worked example
This method is a straightforward translation of the rules of the game. Read the rule and the code side by side.
public static boolean cardMatches(Card card1, Card card2) {
return card1.getSuit() == card2.getSuit()
|| card1.getRank() == card2.getRank()
|| card1.getRank() == 8;
}| clause | the rule it encodes |
|---|---|
| card1.getSuit() == card2.getSuit() | must match the suit |
| || card1.getRank() == card2.getRank() | or the rank |
| || card1.getRank() == 8 | or be an eight — a wild card |
The rule has three clauses.
Why: The card must match the rank or suit of the previously played card, or be an eight, which is a wild card.
Each becomes one comparison.
Why: Joined with ||, which is Lesson 5b's or.
Only card1 is checked for being an eight.
Why: card1 is the candidate; card2 is what is already on the pile.
It is static.
Why: cardMatches is a static method, also defined in Player — it reads no instance variable, only its two parameters.
Verify: Check the 8 of Clubs against the King of Hearts: suits differ, ranks differ, and the third clause makes it legal.
Why: A method that reads like the rule it implements is the goal. Exercise 14.4 notes the awkwardness: cardMatches is a static method in Player, but it would be more natural if it were an instance method in Card — except that Card is meant to work for any game, so the rule cannot live there.
Prediction
Apply the three clauses.
Card prev = new Card(13, 2); // King of Hearts
Card card = new Card(8, 0); // 8 of Clubs
cardMatches(card, prev)| clause | result |
|---|---|
| same suit? 0 vs 2 | no |
| same rank? 8 vs 13 | no |
| card1 is an eight? | ? |
Predict first
What does cardMatches return?
Correct: true — the third clause makes any eight legal
Why: The three clauses are joined with ||, so any one of them is enough — and card1.getRank() == 8 is true. That is the wild-card rule the game is named after, and it is the reason the drawForMatch loop can assume a match will eventually turn up.
Concept
The book flags its own compromise. When we designed the program for this chapter, we tried to minimize the number of classes. As a result, we ended up with a few awkward methods.
| option | problem |
|---|---|
| an instance method in Card | Card would know a Crazy Eights rule |
| a static method in Player | awkward — it is about cards, not players |
| a match method in an EightsCard subclass | the clean answer |
The problem is that Card is supposed to be useful for any card game, not just Crazy Eights. You can solve this problem by adding a new class, EightsCard, that extends Card and provides a method, match. That is Exercise 14.4 — and it is inheritance used to add a game's rules without contaminating the reusable class.
Trap
A reusable class that knows one game's rules.
public class Card {
public boolean matches(Card that) {
return this.suit == that.suit
|| this.rank == that.rank
|| this.rank == 8; // Crazy Eights only
}
}| game | does this method make sense? |
|---|---|
| Crazy Eights | yes |
| Poker | no |
| War | no |
| any future game | it is there anyway |
Every program using Card now carries a rule about eights. The class stops being about cards and starts being about one game — and the next game has to work around it.
Subclass for the game, or keep the rule in the game's own class.
// Exercise 14.4's answer:
public class EightsCard extends Card {
public boolean match(EightsCard that) { ... }
public int scoreCard() { ... }
}
// the book's own compromise: a static method in Player
public static boolean cardMatches(Card c1, Card c2) { ... }| class | knows about eights? |
|---|---|
| Card | no — reusable |
| EightsCard | yes — that is its job |
| Player | yes |
Inheritance lets a game extend the general classes rather than change them. Exercise 14.4 also suggests an EightsHand with a scoreHand method — the same move applied one level up.
Definition probe
Reusability is the deciding question.
Sort into buckets
Sort each method.
Fill the middle
Suit, or rank, or a wild eight.
Fill in the blanks
return card1.getSuit() == card2.getSuit()
|| card1.getRank() == card2.getRank()
|| card1.getRank() == 8;
Why: The three conditions are alternatives, so they are joined with || — any one makes the play legal. Only card1 is checked for being an eight, because card1 is the card being played and card2 is what is already on top of the discard pile.
Edge cases
We'll assume for now that it always finds one.
Discussion prompt
The loop only exits on a match. Construct the situation where it never would, and say why it does not arise in practice.
Hint: How many eights are there, and what does drawCard do when the pile empties?
Answer:
It would need every remaining card to fail all three clauses — no card of the same suit, none of the same rank, and no eight anywhere available.
It cannot happen in practice: there are four eights and thirteen cards of each suit, and drawCard reshuffles the discard pile when the draw pile empties, so cards keep coming.
But the loop would spin forever rather than fail, which is worth noticing. The book's parenthesis is an assumption stated honestly — and an assumption stated is much better than one buried.
Section
Section 14.5
Concept
In Section 13.2 we introduced top-down design. In this way of developing programs, we identify high-level goals and break them into smaller problems. In this section, we present bottom-up design, which goes the other way around: first we identify simple pieces we need and then we assemble them into more-complex algorithms.
// looking at the rules, we can identify some methods we'll need:
// set up the game (Eights constructor)
// check whether the game is over (isDone)
// shuffle the discard pile into the draw pile (reshuffle)
// draw a card, reshuffling if necessary (drawCard)
// switch from one player to the next (nextPlayer)
// display the state and wait for input (displayState)| top-down | bottom-up | |
|---|---|---|
| start with | the outline | the pieces |
| discover | which helpers you need | how they assemble |
| good when | you know the shape | you know the parts |
| Chapter | 13 | 14 |
bottom-up design — A way of developing programs by identifying simple pieces, implementing them first, and then assembling them into more-complex algorithms.
Looking at the rules of Crazy Eights, we can identify some of the methods we'll need. The rules are already a list of things that must happen — so reading them off is faster than inventing an outline.
Notation
Before any method, the attributes. Here is the beginning of the class definition for Eights, which encapsulates the state of the game.
Annotate
Reading a class's attributes tells you what it is responsible for. Eights owns the piles and the players; it does not own a strategy, which is why play lives in Player.
Worked example
If the draw pile ever runs out, the discard pile is shuffled (except the top card) and becomes the new draw pile. Four lines, and the order of them is the whole method.
public void reshuffle() {
Card prev = discardPile.popCard();
discardPile.dealAll(drawPile);
discardPile.addCard(prev);
drawPile.shuffle();
}
public Card drawCard() {
if (drawPile.isEmpty()) {
reshuffle();
}
return drawPile.popCard();
}| line | discardPile | drawPile |
|---|---|---|
| start | n cards | 0 |
| popCard() saves the top | n − 1 | 0 |
| dealAll(drawPile) | 0 | n − 1 |
| addCard(prev) | 1 — the saved card | n − 1 |
| drawPile.shuffle() | 1 | n − 1, shuffled |
Save the top card first.
Why: The first line saves the top card from discardPile.
Move everything else across.
Why: The next line transfers the rest of the cards to drawPile.
Put the saved card back.
Why: Then we put the saved card back into discardPile.
Shuffle the new draw pile.
Why: And shuffle drawPile — which Hand can do only because it inherits shuffle from CardCollection.
Verify: Check that after reshuffle the discard pile has exactly one card and it is the one that was on top.
Why: The top card has to stay — it is what the next player must match against. Dealing it away would leave lastCard() reading a card nobody played, so the ordering of these four lines is not a style choice.
Prediction
It had 30; the draw pile had 0.
Card prev = discardPile.popCard();
discardPile.dealAll(drawPile);
discardPile.addCard(prev);| step | discardPile |
|---|---|
| start | 30 |
| popCard | 29 |
| dealAll | 0 |
| addCard(prev) | ? |
Predict first
How many cards does the discard pile hold?
Correct: 1
Why: Everything but the saved top card moves to the draw pile, and then the saved card goes back — so the discard pile holds exactly one. That card is what the next player must match, which is why the rule says the discard pile is shuffled except the top card.
Concept
Two more pieces, each three or four lines, each hiding one rule from everything that calls it.
public Player nextPlayer(Player current) {
if (current == one) {
return two;
} else {
return one;
}
}| method | hides | callers therefore |
|---|---|---|
| drawCard() | that the pile might need reshuffling | just ask for a card |
| nextPlayer(current) | that there are exactly two players | just ask who is next |
| isDone() | which hand to check | just ask if the game is over |
nextPlayer takes the current player as a parameter and returns the player who should go next. Note current == one compares references — which is right here, because the question really is is this the same object, not is this an equal player. That is Lesson 11b's identity, being the correct choice for once.
Trap
Reshuffling everything, including the card in play.
public void reshuffle() {
discardPile.dealAll(drawPile); // the top card goes too
drawPile.shuffle();
}| after | discardPile | problem |
|---|---|---|
| dealAll | 0 cards | nothing to match against |
| takeTurn calls lastCard() | — | no last card |
| the game | — | breaks |
The next takeTurn reads discardPile.lastCard() to find what must be matched — and there is no card to read. **The rule says except the top card for exactly this reason.**
Save it, move the rest, put it back.
Card prev = discardPile.popCard();
discardPile.dealAll(drawPile);
discardPile.addCard(prev);
drawPile.shuffle();| invariant | held? |
|---|---|
| the discard pile always has a top card | yes |
| every other card becomes available again | yes |
| cards are conserved | yes |
Three lines to preserve one card, and the game depends on all three. This is the kind of detail that reads as fussy until you write the version without it and watch the game fail on the first reshuffle.
Definition probe
Two design processes, used in two chapters.
Sort into buckets
Sort each description.
Fill the middle
The pile might be empty.
Fill in the blanks
public Card drawCard() isEmpty}()) reshuffle}();
}
return drawPile.popCard();
}
Why: Checking before popping means no caller ever has to think about an empty pile — the rule about reshuffling is hidden inside this one method. That is why drawForMatch can call eights.drawCard() in a loop without any special case of its own.
Explain it to yourself
Chapter 13 used top-down for shuffle.
Discussion prompt
Why did the rules of Crazy Eights suit bottom-up design, when shuffling suited top-down?
Hint: Where did the list of methods come from in each case?
Answer:
Because the rules are already a list of things that must happen: deal, check for a winner, reshuffle when the pile runs out, take turns. The pieces were given.
Shuffling was the opposite — one goal with no obvious parts, so you had to write the outline first and let it name the helpers.
The result is similar either way: we have a high-level method that calls helper methods. The difference is the development process we used to arrive at this solution. Which one to use depends on whether you know the shape or the parts.
Section
Section 14.5
Concept
Using these pieces, we can write takeTurn, which executes one player's turn. Five lines, and every one of them calls something written earlier.
public void takeTurn(Player player) {
Card prev = discardPile.lastCard();
Card next = player.play(this, prev);
discardPile.addCard(next);
System.out.println(player.getName() + " plays " + next);
System.out.println();
}| line | does | defined in |
|---|---|---|
| discardPile.lastCard() | read the card to match | CardCollection |
| player.play(this, prev) | the player chooses | Player |
| discardPile.addCard(next) | play the card | CardCollection |
| println | report it | — |
It reads the top card off the discard pile and passes it to player.play, which you saw in the previous section. The result is the card the player chose, which is added to the discard pile. Note lastCard rather than popCard — the card stays on the pile.
Picture it
Five objects cooperate and each one does only its own job.
Figure (svg): A pipeline showing a turn passing from the discard pile to the player and back
The Eights object never looks at a card's rank or suit, and the Player never touches the draw pile directly. Each asks the other for what it needs — which is what the two-way play(this, prev) call is for.
Worked example
Finally, we use takeTurn and the other methods to write playGame. Every piece from the bottom-up list appears exactly once.
public void playGame() {
Player player = one;
// keep playing until there's a winner
while (!isDone()) {
displayState();
takeTurn(player);
player = nextPlayer(player);
}
// display the final score
one.displayScore();
two.displayScore();
}| iteration | player | after the turn |
|---|---|---|
| 1 | one | player becomes two |
| 2 | two | player becomes one |
| 3 | one | ... |
| last | either | isDone() is true |
Start with player one.
Why: Player player = one;
Loop until somebody wins.
Why: while (!isDone()) — keep playing until there's a winner.
Each iteration is show, play, switch.
Why: displayState, takeTurn, then player = nextPlayer(player).
Then score.
Why: Display the final score with one.displayScore() and two.displayScore().
Verify: Read playGame and check that no line of it mentions a card, a rank, or a suit.
Why: Done! The result of bottom-up design is similar to top-down: we have a high-level method that calls helper methods. Twelve lines, and every one names a piece rather than doing work — which is what makes the loop readable as the rules of the game.
Prediction
isDone checks both hands.
public boolean isDone() {
return one.getHand().isEmpty() || two.getHand().isEmpty();
}| hand one | hand two | isDone |
|---|---|---|
| 3 cards | 5 cards | false |
| 0 cards | 5 cards | true |
Predict first
What ends the game?
Correct: Either player running out of cards
Why: The || means one empty hand is enough — as soon as a player has no cards, the game ends, and all other players score penalty points for their remaining cards. The draw pile running out triggers a reshuffle rather than ending anything.
Concept
player = nextPlayer(player) is worth a second look — it alternates without any index or modulo arithmetic.
Player player = one;
while (!isDone()) {
takeTurn(player);
player = nextPlayer(player); // one -> two -> one -> two
}| approach | for two players | for n players |
|---|---|---|
| nextPlayer with an if | clear | does not extend |
| an index and i = (i + 1) % n | works | extends |
| what Exercise 14.3 asks for | — | an ArrayList of players |
Modify the Eights class to create an ArrayList of players, and modify nextPlayer to select the next player — that is Exercise 14.3, and notice that only nextPlayer and the constructor would change. playGame and takeTurn are already written in terms of the next player, so they need no edit at all.
Trap
popCard removes; lastCard reads.
public void takeTurn(Player player) {
Card prev = discardPile.popCard(); // takes it OFF the pile
Card next = player.play(this, prev);
discardPile.addCard(next);
}| method | returns the top card? | leaves it there? |
|---|---|---|
| lastCard() | yes | yes |
| popCard() | yes | no |
The discard pile shrinks every turn while also growing — and the card that was in play vanishes from the game. Cards stop being conserved, and reshuffle eventually finds nothing to move.
Read it, do not take it.
Card prev = discardPile.lastCard();
Card next = player.play(this, prev);
discardPile.addCard(next);| after a turn | discard pile |
|---|---|
| before | n cards |
| after | n + 1 — the played card on top |
| the old top | still there, underneath |
The discard pile only ever grows, until a reshuffle empties it deliberately. That is why CardCollection provides both lastCard and popCard — the distinction between reading and removing is exactly the distinction this method depends on.
Prediction
It reads one card and adds another.
Card prev = discardPile.lastCard();
Card next = player.play(this, prev);
discardPile.addCard(next);| variable | where it came from |
|---|---|
| prev | the top of the discard pile, not removed |
| next | the player's chosen card |
Predict first
Which card is added?
Correct: next — the card the player chose, which came from their hand or the draw pile
Why: prev is only read, so it stays where it is; next is what play returned, already removed from the player's hand by popCard or drawn from the pile. The discard pile grows by exactly one card per turn.
Definition probe
State against strategy.
Sort into buckets
Sort each responsibility.
Counterexample
Exercise 14.3 asks for exactly that.
Discussion prompt
Look at playGame. How much of it would change to support four players, and which method takes the damage?
Hint: Which line mentions a specific player?
Answer:
The loop itself barely changes. It says take a turn, then move to the next player — a sentence that is already true for any number.
The changes land in the constructor (an ArrayList of players instead of two fields), nextPlayer (index arithmetic instead of an if), isDone, and the final scoring.
A well-chosen helper method absorbs a change that would otherwise spread. nextPlayer exists precisely so that playGame never has to know how many players there are — which is why bottom-up design named it as a piece before the loop was written.
Section
Section 14.6
Concept
This chapter demonstrates two common relationships between classes. You have used both; now they have names.
HAS-A — A relationship between two classes in which one class has an instance of another class as one of its attributes.
IS-A — A relationship between two classes in which one class extends another; the subclass is an instance of the superclass.
| relationship | definition | example |
|---|---|---|
| composition | instances of one class contain references to instances of another | an Eights contains two Players, two Hands and a Scanner |
| inheritance | one class extends another class | Hand extends CardCollection |
| HAS-A | the name for composition | Eights has a Scanner |
| IS-A | the name for inheritance | Hand is a CardCollection |
This vocabulary provides a concise way to talk about an object-oriented design. Two words settle a question that otherwise takes a paragraph — and they are the test Lesson 14a used before they had names.
Picture it
Figure 14.1 shows the classes defined in this chapter and the relationships among them. Two kinds of arrow, and they mean different things.
Figure (svg): A UML diagram of Eights, Player, Hand, Deck, CardCollection and Card with both kinds of relationship arrow
Composition arrows have a standard arrowhead, and inheritance arrows have a hollow triangle head, usually pointing up. UML is an international standard, so almost any software engineer in the world could look at this diagram and understand our design.
Worked example
Every arrow in the diagram is a sentence. Say each one and check it is true.
Eights HAS-A Player "an Eights has a Player"
Eights HAS-A Hand "an Eights has a draw pile"
Eights HAS-A Scanner "an Eights has a Scanner"
Player HAS-A Hand "a Player has a Hand"
Hand IS-A CardCollection "a Hand is a CardCollection"
Deck IS-A CardCollection "a Deck is a CardCollection"| sentence | true? | so |
|---|---|---|
| a Player has a Hand | yes | an instance variable |
| a Player is a Hand | no | not extends |
| a Hand is a CardCollection | yes | extends |
| a Hand has a CardCollection | no | it is one |
Write the sentence both ways.
Why: X has a Y and X is a Y — usually only one is true.
The true one names the tool.
Why: has a means an instance variable; is a means extends.
Check every method still makes sense.
Why: If is a is true but some inherited method is nonsense on the subclass, it is not really IS-A.
Draw the arrow accordingly.
Why: Plain head for HAS-A, hollow triangle for IS-A.
Verify: Try an Eights is a Player and a Card has a Hand and notice how obviously wrong they sound.
Why: The sentence test is not a rule of thumb — it is the definition. IS-A means substitutable, which is exactly what Lesson 14a's parameter rule was about, and HAS-A means contained, which is what an instance variable is.
Definition probe
Say the sentence out loud.
Sort into buckets
Sort each pair from this chapter.
Concept
Three chapters, six classes, and every one of them holds a decision the others do not need to know.
| class | relationship to the others | introduced in |
|---|---|---|
| Card | held by collections | Chapter 12 |
| CardCollection | the superclass of Deck and Hand | Chapter 14 |
| Deck | IS-A CardCollection | Chapter 14 |
| Hand | IS-A CardCollection | Chapter 14 |
| Player | HAS-A Hand | Chapter 14 |
| Eights | HAS-A two Players and two Hands | Chapter 14 |
And class diagrams are only one of many graphical representations defined in the UML standard. The diagram is worth drawing before the code as well as after — it is where a wrong relationship shows up cheaply.
Trap
An inheritance arrow pointing from the superclass down.
// wrong reading:
CardCollection ---|> Hand "a CardCollection is a Hand"
// and a plain arrow used for inheritance:
Hand -----------> CardCollection| arrow | means | reads as |
|---|---|---|
| hollow triangle head | inheritance | the source IS-A the target |
| standard arrowhead | composition | the source HAS-A the target |
| direction | always subclass to superclass | specific to general |
Both errors say something false. Not every CardCollection is a Hand — that is exactly the direction Lesson 14a showed does not work.
Subclass to superclass, hollow head, usually pointing up.
Hand -----------|> CardCollection "a Hand IS-A CardCollection"
Player ---------> Hand "a Player HAS-A Hand"| head | direction | |
|---|---|---|
| inheritance | hollow triangle | subclass to superclass |
| composition | standard arrowhead | container to contained |
Inheritance arrows have a hollow triangle head, usually pointing up — so the general classes sit at the top of the page and the specific ones below. That convention alone makes a diagram readable at a glance.
Prediction
Two notations, two meanings.
// Hand extends CardCollection
// Player has a Hand| relationship | arrowhead |
|---|---|
| inheritance | hollow triangle |
| composition | standard |
Predict first
How is Hand's relationship to CardCollection drawn?
Correct: An arrow from Hand to CardCollection with a hollow triangle head
Why: Inheritance arrows have a hollow triangle head, usually pointing up, and they run from the subclass to the superclass — the specific pointing at the general. Reversing it would claim every CardCollection is a Hand, which is the direction that does not hold.
Matching
The chapter's vocabulary.
Match the pairs
Why: These are the chapter's four vocabulary entries. Note that bottom-up design sits alongside top-down from Chapter 13 and incremental development from Chapter 4 — three named processes, and you choose by whether you know the shape, the parts, or neither.
Real world
UML is an international standard.
Discussion prompt
Everyone could invent their own way of drawing classes. What does a standard notation buy that a clear hand-drawn diagram does not?
Hint: Who is reading it, and did you meet them?
Answer:
Almost any software engineer in the world could look at this diagram and understand our design — with no legend and no explanation from you.
A private notation needs teaching every time. A standard one travels: into a document, a whiteboard, an interview, a codebase you join years later.
The same argument as naming conventions and as toString. Following the shared form costs nothing and means the next reader spends their attention on your design rather than on your notation.
Comparison
Fill the blanks.
Comparison matrix
| incremental | top-down | bottom-up | |
|---|---|---|---|
| start from | a tiny working program | a high-level outline | the simple pieces |
| the example in this book | distance, in Section 4.10 | shuffle, in Section 13.2 | Eights, in Section 14.5 |
| what you discover | that each step works | which helper methods you need | how the pieces assemble |
| the result | a high-level method calling helpers | the same | the same |
The last row is the point. The result of bottom-up design is similar to top-down: we have a high-level method that calls helper methods. The difference is the development process we used to arrive at this solution.
Pattern
Separate what a thing is from what a game does with it, and use the two relationships accordingly.
// reusable: knows nothing about any game
public class Card { ... }
public class CardCollection { ... }
public class Hand extends CardCollection { ... } // IS-A
// game-specific: knows the rules
public class Player {
private Hand hand; // HAS-A
public Card play(Eights eights, Card prev) { ... }
public static boolean cardMatches(Card c1, Card c2) {
return c1.getSuit() == c2.getSuit()
|| c1.getRank() == c2.getRank()
|| c1.getRank() == 8; // the rule lives HERE
}
}| question | answer | tool |
|---|---|---|
| is X a kind of Y? | yes | extends |
| does X contain a Y? | yes | an instance variable |
| is this a rule of one game? | yes | a game class, or a subclass |
| would every game want it? | yes | the reusable class |
Check
Work it out before you click.
public class Player {
private String name;
private Hand hand;
}
public class Hand extends CardCollection { ... }| pair | relationship |
|---|---|
| Player and Hand | ? |
| Hand and CardCollection | ? |
Check your understanding
What are the two relationships?
Answer: A
Why: A Player contains a Hand as an attribute — composition, or HAS-A. Hand extends CardCollection — inheritance, or IS-A, which also means a Hand can be passed wherever a CardCollection is expected. Saying each sentence aloud settles it: a Player has a Hand is true; a Player is a Hand is not.
Player extends Hand, which would make every Player usable as a card collection.extends keyword is in the code.Check
Work it out before you click.
public void reshuffle() {
Card prev = discardPile.popCard();
discardPile.dealAll(drawPile);
discardPile.addCard(prev);
drawPile.shuffle();
}| line | purpose |
|---|---|
| popCard | ? |
| addCard(prev) | ? |
Check your understanding
Why are the first and third lines there?
Answer: A
Why: The rule says the discard pile is shuffled except the top card — that card is what the next play must match. Without saving it, takeTurn's discardPile.lastCard() would find an empty pile. Three lines to preserve one card, and the game depends on all three.
Check
Work it out before you click.
Card prev = discardPile.lastCard();
Card next = player.play(this, prev);
discardPile.addCard(next);| method | removes the card? |
|---|---|
| lastCard() | no |
| popCard() | yes |
Check your understanding
Why lastCard rather than popCard on the first line?
Answer: A
Why: prev is the card the player must match, and it belongs on the discard pile with the new card played on top of it. Using popCard would remove it from the game entirely, so cards would stop being conserved and reshuffle would eventually find nothing to move. Providing both methods is exactly so this distinction can be made.
Real world
Design a better strategy for the Player.play method. Write a new class that extends Player and overrides play to implement your strategy.
Discussion prompt
Exercise 14.2 suggests a Genius class that extends Player and plays better. Why is that a better design than adding an if to Player — and where else does the same pattern appear?
Hint: How would you compare two strategies?
Answer:
Because you can then play them against each other: replace one player with a Genius, run a hundred games, and count. An if inside Player gives you one behaviour at a time.
And the game code needs no change at all. A Genius is a Player, so takeTurn(player) works unmodified — which is the substitution rule doing real work rather than being an example.
The pattern appears everywhere a program must choose between interchangeable behaviours — sorting orders, compression methods, AI difficulty levels. Make the varying part a subclass, and the code around it stops needing to know which one it has.
Commit first
Commit to an answer and to your confidence.
Predict first
What is the difference between top-down and bottom-up design?
Correct: Top-down starts with a high-level outline and discovers the helpers; bottom-up starts with the pieces and assembles them
Why: In top-down design we identify high-level goals, like shuffling a deck, and break them into smaller problems. Bottom-up design goes the other way around: first we identify simple pieces we need and then we assemble them into more-complex algorithms. The finished code looks the same either way — a high-level method that calls helper methods — and the choice depends on whether you know the shape or the parts. Crazy Eights suited bottom-up because the rules already listed the pieces.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate is designing a program and asks whether class A should extend class B or contain one. Give them the test, and one example of each from this chapter.
Hint: Two sentences, and only one of them is true.
Answer:
Say both sentences out loud: A is a B, and A has a B. Usually only one sounds true, and that one names the tool — is a means extends, has a means an instance variable.
A Hand is a CardCollection, so Hand extends it — and that means a Hand can be passed anywhere a CardCollection is expected. A Player has a Hand, so Player just holds one as a field.
One extra check for is a: every inherited method must still make sense on the subclass. If any of them would be nonsense, it is not really IS-A. That check is what stops inheritance being used just to reuse code.
Exit ticket
One question before you close the deck.
Predict first
Why do Card, CardCollection, Deck and Hand know nothing about Crazy Eights?
Correct: So they can be reused for any card game — the rules live in Player and Eights instead
Why: Think Java says it directly: the Deck and Hand classes we have defined so far could be used for any card game, and that's probably a good thing, since it makes it easy to reuse these classes if we want to make another game in the future. It is why cardMatches sits awkwardly in Player as a static method rather than comfortably in Card — and why Exercise 14.4's answer is an EightsCard subclass, which adds the rule without contaminating the general class.
Connect it up
One page, from memory.
Draw it
Draw the six-class UML diagram — Eights, Player, CardCollection, Deck, Hand, Card — using hollow triangle heads for IS-A and plain heads for HAS-A, and write each arrow's sentence beside it. Then write playGame's twelve lines and mark, for each call, which class defines it. Finish by writing cardMatches's three clauses next to the three sentences of the rule they encode, and one line on why that method is not in Card.
Recap
Three sections that add the rules, name the design process, and name the relationships.
| if you remember one thing | it is this |
|---|---|
| about design | the rules belong outside the reusable classes |
| about process | top-down and bottom-up meet in the middle |
| about relationships | say the sentence aloud; only one is true |
Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.