Deck and Pile were two versions of the same code, so this lesson merges them into a CardCollection and then uses `extends` to put back the differences: a Deck is a CardCollection with a different constructor, and a Hand is a CardCollection with one extra method. Plus the substitution rule that makes it all worth it. Follows Think Java 2e, Chapter 14 (Extending Classes), Sections 14.1-14.3, pp. 233-240, cross-referenced against The Java Tutorials — Inheritance.
Subject: Java · 65 slides · code lesson
Open the interactive version of this deck
Title
Think Java 2e · Chapter 14 · Extending Classes
Sections 14.1-14.3 · pp. 233-240
Objectives
This lesson follows Think Java 2e, Chapter 14 (Extending Classes), Sections 14.1-14.3, pp. 233-240. Everything on these slides can be checked against those pages.
1. Recognise duplication between two classes as the signal to extract a common superclass.
2. Write a class that extends another, and use super to invoke the superclass constructor.
3. Explain what is inherited and what is not.
4. State the substitution rule: a subclass object can be used wherever the superclass is expected.
5. Explain why the reverse is not true, using the cat–mammal–animal analogy.
6. Write a method that moves cards between two collections of possibly different subclasses.
Warm-up
The end of Lesson 13b noticed something. This chapter acts on it.
Discussion prompt
From Lesson 13b: what did Deck and Pile have in common, and what was the one thing that forced them to use different storage? And what is a wrapper method?
Hint: Both hold cards. One changes size.
Answer:
Both are collections of cards with print, add and remove operations. Deck used a Card array because its size never changes; Pile used an ArrayList because a pile grows and shrinks. A wrapper method does little more than call another method, giving it a name that suits the class.
Furthermore, Deck and Pile are essentially two versions of the same code: one based on arrays, and the other based on ArrayList. It would be helpful to combine their features into one class that meets the needs of both. That is this lesson.
Concept
To implement Crazy Eights, we need to represent a deck of cards, a discard pile, a draw pile, and a hand for each player. Four things, all collections of cards, all slightly different — which is exactly the situation inheritance exists for.
Figure (svg): A UML diagram showing Deck and Hand both extending CardCollection with a hollow inheritance arrow
Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 14 (Extending Classes), Sections 14.1-14.3, pp. 233-240 — Chapter 14 opens on printed page 233.
Section
Section 14.1
Concept
The Deck and Pile classes from the previous chapter meet some of these requirements. But unless we make some changes, neither of them represents a hand of cards very well. So we start over with one class.
public class CardCollection {
private String label;
private ArrayList<Card> cards;
public CardCollection(String label) {
this.label = label;
this.cards = new ArrayList<Card>();
}
}| attribute | purpose |
|---|---|
| label | tells one collection from another |
| cards | an ArrayList, so the size can change |
Since this class will represent different piles and hands of cards, we'll add a label attribute to tell them apart. The storage is ArrayList rather than an array, because a hand and a pile both change size — the requirement that split Deck from Pile in the first place.
Notation
Section 14.1 makes a stylistic change and says exactly why it waited until now.
Annotate
this was a teaching aid, not a requirement — cards and this.cards mean the same thing inside an instance method.this.label = label.this for exactly that reason, while addCard drops it.this everywhere, an attribute is obvious; without it, you have to know what is declared where.The rule that never changes: this is required wherever a name would otherwise be shadowed. Everywhere else it is style, and Lesson 11a's shadowing example is still the case that matters.
Worked example
Two versions of the same name, and the second one exists for a reason worth reading.
public Card popCard(int i) {
return cards.remove(i);
}
public Card popCard() {
int i = cards.size() - 1; // from the end of the list
return popCard(i);
}| call | removes | cost |
|---|---|---|
| popCard(0) | the first card | shifts every later card left |
| popCard(3) | the fourth card | shifts the rest left |
| popCard() | the last card | nothing to shift |
The indexed version removes any card.
Why: It takes an index, removes the card at that location, and shifts the following cards left to fill the gap.
The no-argument version removes the last.
Why: If we are dealing cards from a shuffled deck, we don't care which card gets removed. It is most efficient to choose the last one, so we don't have to shift any cards left.
The second calls the first.
Why: return popCard(i); — one implementation, two entry points.
That is overloading, from Lesson 11a.
Why: Same name, different parameter lists, and Java picks by the call.
Verify: Pop from a 52-card collection both ways and reason about how many elements move each time.
Why: When the order does not matter, removing from the end is free and removing from the front costs a shift of the whole list. That is a real performance argument, and it is the reason the convenient version is the one that takes no argument — the cheap thing should also be the easy thing to type.
Prediction
It could just as easily take from the front.
public Card popCard() {
int i = cards.size() - 1;
return popCard(i);
}| remove from | elements that must shift |
|---|---|
| index 0 | all the rest |
| the last index | none |
Predict first
What is the reason?
Correct: Removing from the end shifts nothing, and when the deck is shuffled it does not matter which card you get
Why: It is most efficient to choose the last one, so we don't have to shift any cards left. The shuffling is what makes the choice free: any card is as random as any other, so you may as well take the cheap one.
Concept
To access the elements of an ArrayList, you can't use the array [] operator. Instead, you have to use the methods get and set. So CardCollection provides wrappers.
public boolean isEmpty() {
return cards.isEmpty();
}
public int size() {
return cards.size();
}
public Card getCard(int i) {
return cards.get(i);
}
public Card lastCard() {
int i = cards.size() - 1;
return cards.get(i);
}| method | wraps | why the name is better |
|---|---|---|
| isEmpty() | cards.isEmpty() | asks about the collection, not the list |
| size() | cards.size() | same |
| getCard(i) | cards.get(i) | says what kind of thing comes back |
| lastCard() | cards.get(size - 1) | hides the size − 1 |
lastCard gets the last card but doesn't remove it — which is what takeTurn will need to read the top of the discard pile. Note that it also hides size() - 1, so that off-by-one lives in one place.
Trap
A set wrapper would let anyone overwrite any card.
// NOT provided, deliberately:
public void setCard(int i, Card card) {
cards.set(i, card);
}
// which would allow:
hand.setCard(0, new Card(8, 2)); // conjure an eight from nowhere| with setCard | consequence |
|---|---|
| overwrite any position | a card can be replaced by any other |
| no card is removed | cards can be duplicated or destroyed |
| the deck's contents | no longer a permutation of 52 cards |
The invariant that matters in a card game is that the 52 cards are conserved — moved around, never created or destroyed. A public set breaks it silently.
Provide only the modifiers the class can vouch for.
public void swapCards(int i, int j) {
Card temp = cards.get(i);
cards.set(i, cards.get(j));
cards.set(j, temp);
}
public void shuffle() {
Random random = new Random();
for (int i = cards.size() - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
swapCards(i, j);
}
}| modifier provided | what it preserves |
|---|---|
| popCard(i) and popCard() | removes one card — nothing is created |
| addCard(card) | adds one card — nothing is destroyed |
| swapCards(i, j) | exchanges two — the multiset is unchanged |
| setCard | not provided |
In order to control the ways card collections are modified, we don't provide a wrapper for set. The only modifiers we provide are the two versions of popCard and the following version of swapCards. Every one of them preserves the cards; set would not.
Definition probe
Only some of CardCollection's methods are modifiers.
Sort into buckets
Sort each method.
Fill the middle
getCard hides the list's own method.
Fill in the blanks
public Card getCard(int i) get}(i);
}
public Card lastCard() -} 1;
return cards.get(i);
}
Why: An ArrayList has no [] operator, so element access goes through get. lastCard hides the size() - 1 inside the class, which means that particular off-by-one appears exactly once in the program rather than at every call site.
Explain it to yourself
get has one; set does not.
Discussion prompt
The class wraps get but deliberately not set. What can a program guarantee about a CardCollection because of that omission?
Hint: Count the cards before and after any operation.
Answer:
That cards are conserved. Every modifier the class provides either moves one card out, moves one in, or exchanges two — so the collection's contents are always a rearrangement of cards that already existed.
set would break that: it overwrites, so one card vanishes and another appears. Nothing in a card game should be able to do that.
A class's guarantees come from what it refuses to offer, not only from what it does. Lesson 12a's missing setters made Card immutable by exactly the same move.
Section
Section 14.2
Concept
At this point we have a class that represents a collection of cards. However, each kind of collection will be slightly different. Rather than add every possible feature to CardCollection, we can use inheritance to define subclasses.
public class Deck extends CardCollection {
public Deck(String label) {
super(label);
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
addCard(new Card(rank, suit));
}
}
}
}| part | meaning |
|---|---|
| extends CardCollection | Deck has everything CardCollection has |
| super(label) | invoke the superclass constructor |
| the nested loop | the one thing a Deck adds |
| everything else | inherited, not rewritten |
inheritance — The ability to define a new class that has the same instance variables and methods of an existing class.
subclass — A class that inherits from, or extends, an existing class.
superclass — An existing class that is extended by another class.
A subclass is a class that extends an existing class; that is, it has the attributes and methods of the existing class, plus more. Note how short Deck is: the only additional method in Deck, at least for now, is a constructor.
Notation
Section 14.2 states the rules compactly. All four matter later.
Annotate
Deck extends CardCollection, which in turn extends Object.Card@1a2b3c4d before you wrote a toString is that it inherited Object's.super to run the one it did not inherit.The second and third notes retroactively explain something you met in Chapter 11. toString was not a special name the compiler knows about — it was a method you were overriding all along.
Worked example
The first line of the constructor uses super, which is a keyword that refers to the superclass of the current class.
public Deck(String label) {
super(label); // run CardCollection's constructor first
for (int suit = 0; suit <= 3; suit++) {
for (int rank = 1; rank <= 13; rank++) {
addCard(new Card(rank, suit)); // an inherited method
}
}
}| step | what exists afterwards |
|---|---|
| super(label) called | label set, cards is an empty ArrayList |
| the loop runs | 52 Cards added via the inherited addCard |
| constructor returns | a Deck that is also a CardCollection |
super used as a method invokes a constructor.
Why: When super is used as a method, as in this example, it invokes the constructor of the superclass.
That constructor sets up the inherited state.
Why: super invokes the CardCollection constructor, which initializes the attributes label and cards.
Then the subclass's own work happens.
Why: When it returns, the Deck constructor continues with the loop.
addCard needs no qualification.
Why: It is inherited, so it is called as if it were Deck's own — which, in every sense that matters, it is.
Verify: Remove the super(label); line and see the constructor fail — cards would never be created.
Why: The order is not optional. The superclass's constructor must run first, because the subclass's own code — here addCard — depends on the state it sets up. A Deck that added cards before cards existed would throw a NullPointerException.
Prediction
Deck's body contains only a constructor.
public class Deck extends CardCollection {
public Deck(String label) { ... }
}
Deck deck = new Deck("Deck");
deck.shuffle();| method | defined in |
|---|---|
| shuffle | CardCollection |
| Deck's own methods | just the constructor |
Predict first
Does deck.shuffle() work?
Correct: Yes — Deck inherits every public method of CardCollection
Why: A Deck object has the same instance variables and methods as a CardCollection, so shuffle, addCard, size and the rest are available without a line of code in Deck. Constructors are the one exception — those are not inherited, which is why Deck has to write one.
Concept
We could define two classes, one for hands and one for piles, but there is not much difference between them. So we'll use one class, called Hand, for both hands and piles.
public class Hand extends CardCollection {
public Hand(String label) {
super(label);
}
public void display() {
System.out.println(getLabel() + ": ");
for (int i = 0; i < size(); i++) {
System.out.println(getCard(i));
}
System.out.println();
}
}| method used in display | comes from |
|---|---|
| getLabel() | inherited from CardCollection |
| size() | inherited |
| getCard(i) | inherited |
| display() | Hand's own — the only thing it adds |
In summary, a Deck is just like a CardCollection, but it provides a different constructor. And a Hand is just like a CardCollection, but it provides an additional method, display. Two subclasses, twelve lines between them, and everything else shared.
Trap
A subclass with no constructor cannot be given a label.
public class Hand extends CardCollection {
public void display() { ... }
// no constructor
}
Hand hand = new Hand("Hand"); // compile error| inherited? | member |
|---|---|
| yes | public methods — addCard, size, shuffle |
| yes | the inherited attributes exist in every object |
| no | constructors |
Constructors are not inherited, so Hand(String) does not exist unless Hand writes it. And because CardCollection has no no-argument constructor, Java cannot supply a default either.
Write a constructor that calls super.
public class Hand extends CardCollection {
public Hand(String label) {
super(label);
}
public void display() { ... }
}| line | does |
|---|---|
| public Hand(String label) | declares the constructor clients call |
| super(label) | runs CardCollection's constructor |
| nothing else | Hand adds no state of its own |
A constructor that does nothing but call super is not pointless — it is what makes the superclass's constructor reachable under the subclass's name. Hand's is three lines and all three are necessary.
Definition probe
One category is the exception.
Sort into buckets
Sort each member of CardCollection.
Fill the middle
Hand is a CardCollection with one extra method.
Fill in the blanks
public class Hand extends CardCollection super}(label);
}
}
Why: extends declares the inheritance; super(label) invokes the superclass constructor, which is what creates the ArrayList the inherited methods depend on. It must come first — the subclass's own code runs on state the superclass set up.
Socratic
Then Hand would not be needed at all.
Discussion prompt
display is eight lines. Putting it in CardCollection would delete the Hand class entirely. Why does the book keep them separate?
Hint: What would a Deck then be able to do?
Answer:
Because a Deck would then have a display method too — and printing 52 cards is not something a deck should offer. The method would exist everywhere and be meaningful in one place.
Rather than add every possible feature to CardCollection, the superclass holds only what all collections genuinely share. Each subclass adds what only it needs.
A superclass that accumulates every subclass's features stops being an abstraction and becomes a grab bag. Keeping it minimal is what makes extends worth using rather than just copying code.
Section
Section 14.3
Concept
To begin the game, we need to deal cards to each of the players. And during the game, we need to move cards between hands and piles. One method in CardCollection meets both requirements.
public void deal(CardCollection that, int n) {
for (int i = 0; i < n; i++) {
Card card = popCard();
that.addCard(card);
}
}
public void dealAll(CardCollection that) {
int n = cards.size();
deal(that, n);
}| parameter | role |
|---|---|
| this | the collection cards come from |
| that | the collection they go to |
| n | how many to move |
The deal method removes cards from the collection it is invoked on, this, and adds them to the collection it gets as a parameter, that. Naming the parameter that is Lesson 11b's convention — the other one — and it makes the direction of the transfer readable at a glance.
Picture it
Nothing is created or destroyed. A card leaves one ArrayList and joins another.
Figure (svg): A pipeline showing a card leaving one collection with popCard and entering another with addCard
dealAll is deal with n set to everything, so the two methods are one implementation. Note that dealAll reads cards.size() directly — it is inside CardCollection, so it may.
Worked example
At this point, we can create a Deck and start dealing cards.
Deck deck = new Deck("Deck");
deck.shuffle();
Hand hand = new Hand("Hand");
deck.deal(hand, 5);
hand.display();
Hand drawPile = new Hand("Draw Pile");
deck.dealAll(drawPile);
System.out.printf("Draw Pile has %d cards.\n", drawPile.size());| after | deck | hand | drawPile |
|---|---|---|---|
| new Deck("Deck") | 52 | — | — |
| deck.deal(hand, 5) | 47 | 5 | — |
| deck.dealAll(drawPile) | 0 | 5 | 47 |
Create and shuffle the deck.
Why: Because the deck is shuffled randomly, you should get a different hand each time you run this example.
Deal five to a hand.
Why: deck.deal(hand, 5) — five popCards and five addCards.
Display it.
Why: hand.display() — the method Hand adds.
Deal the rest to a draw pile.
Why: Draw Pile has 47 cards.
Verify: Run it twice and confirm the hand differs each time while the counts do not.
Why: Five plus forty-seven is fifty-two. The counts are the invariant, and the whole point of providing only conserving modifiers in Section 14.1 was to make that arithmetic always work out.
Prediction
deal declares a CardCollection parameter.
public void deal(CardCollection that, int n) { ... }
Hand hand = new Hand("Hand");
deck.deal(hand, 5);| Hand extends | so a Hand is |
|---|---|
| CardCollection | also a CardCollection |
Predict first
Does it compile?
Correct: Yes — a Hand is also a CardCollection, so it can be used wherever one is expected
Why: This is the substitution rule that inheritance buys. A Hand object is also considered to be a CardCollection object, so it satisfies the parameter without a cast — which is what makes one deal method serve dealing, drawing and discarding alike.
Concept
If you are a careful reader, you might notice something strange. deal declares a CardCollection parameter, and the call passes a Hand.
public void deal(CardCollection that, int n) { ... }
Hand hand = new Hand("Hand");
deck.deal(hand, 5); // a Hand, not a CardCollection| a method expecting | accepts |
|---|---|
| CardCollection | a CardCollection, a Deck, or a Hand |
| Hand | a Hand only |
| Deck | a Deck only |
It's because Hand is a subclass of CardCollection, so a Hand object is also considered to be a CardCollection object. If a method expects a CardCollection, you can give it a Hand, a Deck, or a CardCollection. One object, more than one type.
Trap
A CardCollection is not a Hand.
public void showIt(Hand h) {
h.display();
}
CardCollection cc = new CardCollection("misc");
showIt(cc); // compile error| object | is a CardCollection? | is a Hand? |
|---|---|---|
| new Hand(...) | yes | yes |
| new Deck(...) | yes | no |
| new CardCollection(...) | yes | no |
But it doesn't work the other way around: not every CardCollection is a Hand, so if a method expects a Hand, you have to give it a Hand, not a CardCollection or a Deck. A plain CardCollection has no display method for the parameter to promise.
Declare the parameter as loosely as the method allows.
// needs only addCard - so ask for the superclass
public void deal(CardCollection that, int n) { ... }
// needs display - so it must ask for a Hand
public void showIt(Hand h) { h.display(); }| the method uses | so the parameter should be |
|---|---|
| addCard, size | CardCollection |
| display | Hand |
| nothing card-specific | the loosest type that works |
Ask for the least you need. deal uses only addCard, so declaring CardCollection lets it move cards between a deck, a hand and a pile alike — which is exactly why one method covers dealing, drawing and discarding.
Definition probe
Substitution runs one way only.
Sort into buckets
Sort each call, given void f(CardCollection c) and void g(Hand h).
Matching
One object can belong to more than one type.
Match the pairs
Why: Every object belongs to its own class and to every class above it in the chain — and since classes with no extends inherit from java.lang.Object, everything is an Object. That is why Object's toString was what printed Card@1a2b3c4d back in Lesson 11b.
Real world
If it seems strange that an object can belong to more than one type, remember that this happens in real life too.
Discussion prompt
The book's analogy is cats, mammals and animals. Work out which direction the substitution runs, and name a sentence that is true one way and false the other.
Hint: Think about a sign saying no animals against one saying no cats.
Answer:
Every cat is also a mammal, and every mammal is also an animal. But not every animal is a mammal, and not every mammal is a cat.
A rule that applies to animals applies to your cat; a rule about cats says nothing about a dog. The specific satisfies the general, never the reverse.
That is exactly the parameter rule. A method that asks for a CardCollection will accept a Hand; a method that asks for a Hand will not accept a CardCollection — because it might call display, which only a Hand has.
Section
Sections 14.1-14.2 together
Concept
Chapter 13 had Deck and Pile, each with its own storage, its own add, its own remove, its own isEmpty. Chapter 14 has one of each and two small subclasses.
| method | Chapter 13 | Chapter 14 |
|---|---|---|
| add a card | in Deck and in Pile | once, in CardCollection |
| remove a card | in Deck and in Pile | once |
| isEmpty | in Deck and in Pile | once |
| shuffle | in Deck only | once, available to all |
| build 52 cards | in Deck | in Deck — genuinely specific |
| display | in Pile | in Hand — genuinely specific |
The last two rows are what a subclass is for. Everything above them was duplication that had no reason to exist — and Hand now gets shuffle for free, which Pile never had.
Picture it
Three classes, two arrows, and a fourth class above them all that you never wrote.
Figure (svg): A UML inheritance chain from Object down to CardCollection and then to Deck and Hand
Deck extends CardCollection, which in turn extends Object — and you never wrote that last arrow. Every class has it, which is why every object has a toString before you write one.
Worked example
Adding a feature now means choosing a class, and the question is always the same.
// where does each of these belong?
// sortByRank() - useful for a hand, a deck, any collection
// dealFive(that) - a Crazy Eights rule
// buildFullDeck() - only a deck
// display() - only a hand or pile| method | belongs in | because |
|---|---|---|
| sortByRank | CardCollection | every collection could want it |
| display | Hand | a deck should not offer it |
| building 52 cards | Deck's constructor | only a deck is a full deck |
| a game rule | neither | Chapter 14b puts it in Player |
Ask who could use it.
Why: If every subclass could, it belongs in the superclass.
Ask who should not have it.
Why: If giving it to Deck would be wrong, it does not belong in CardCollection.
Ask whether it is a game rule.
Why: The Deck and Hand classes we have defined so far could be used for any card game.
Keep the superclass minimal.
Why: Rather than add every possible feature to CardCollection, only the shared parts go there.
Verify: Put display in CardCollection and ask yourself what deck.display() should print.
Why: 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. Keeping game rules out of the card classes is what makes them reusable, which is the argument Lesson 5b called generalisation.
Definition probe
Say the relationship out loud first.
Sort into buckets
Sort each pair.
Concept
Chapter 13's Pile had an ArrayList. Chapter 14's Hand is a CardCollection. Those are the two ways one class can use another.
| composition — HAS-A | inheritance — IS-A | |
|---|---|---|
| example | Pile has an ArrayList | Hand is a CardCollection |
| written as | an instance variable | extends |
| gets the methods? | no — you write wrappers | yes — automatically |
| substitutable? | no | yes |
CardCollection uses both: it has an ArrayList, and Hand is a CardCollection. Lesson 14b names these relationships properly — but the distinction is already doing work here, since it is exactly why Pile needed nine wrapper methods and Hand needs none.
Trap
Inheritance for convenience, without the IS-A relationship.
// "Player needs a hand, and Hand has all the methods I want"
public class Player extends Hand {
private String name;
}
// which now means:
someMethod(aCardCollection); // a Player can be passed here| claim | true? |
|---|---|
| a Player has a hand | yes |
| a Player is a hand | no |
| a Player can be dealt into | nonsense |
| does it compile? | yes — that is the problem |
The code works, and the design is now a lie. Every method expecting a CardCollection would accept a Player, which no one intended and nothing prevents.
Composition when it is HAS-A.
public class Player {
private String name;
private Hand hand; // a Player HAS a hand
public Player(String name) {
this.name = name;
this.hand = new Hand(name);
}
}| relationship | the right tool |
|---|---|
| a Hand is a CardCollection | extends |
| a Player has a Hand | an instance variable |
| an Eights game has two Players | instance variables |
Read the relationship out loud. A Hand is a CardCollection is true; a Player is a Hand is not. That sentence test is the whole rule, and Lesson 14b's Player class is written exactly this way.
Prediction
It prints every card in the collection.
public void display() {
System.out.println(getLabel() + ": ");
for (int i = 0; i < size(); i++) {
System.out.println(getCard(i));
}
}| if it were in | then |
|---|---|
| CardCollection | deck.display() prints 52 cards |
| Hand | only hands and piles have it |
Predict first
Why is it in Hand rather than CardCollection?
Correct: Because a Deck should not offer it — the superclass holds only what all collections share
Why: Rather than add every possible feature to CardCollection, only genuinely common behaviour goes in the superclass. Printing a deck's fifty-two cards is not something a deck should invite, and a superclass that accumulates every subclass's features stops being an abstraction.
Fill the middle
deal takes from this and gives to that.
Fill in the blanks
public void deal(CardCollection that, int n) popCard}();
that.addCard(card);
}
}
Why: popCard() with no argument takes from the end, which shifts nothing; that.addCard(card) appends it to the destination. Because the parameter is declared as CardCollection, the same method deals to a Hand, a Deck or a plain CardCollection.
Counterexample
Inheritance removed a lot of duplication.
Discussion prompt
CardCollection could absorb display, scoring, matching rules and everything else, leaving Deck and Hand as empty shells. What would be lost?
Hint: What can a Deck then do?
Answer:
Every collection would gain every method, so a Deck could be displayed as a hand and a hand could be built as a deck. The type would stop telling you anything.
And the game rules would be baked into the card classes, so a different card game could not reuse them — losing exactly the reusability the chapter is careful to preserve.
Removing duplication is not the goal; it is a consequence of putting each thing in one right place. Code that is shared because it belongs together is good; code merged because it happens to look alike is not.
Section
Sections 14.1-14.3 in practice
Concept
With inheritance, a call like hand.shuffle() names a method that is nowhere in the Hand class. Knowing where to look is a skill worth practising.
Hand hand = new Hand("Hand");
hand.display(); // defined in Hand
hand.shuffle(); // inherited from CardCollection
hand.toString(); // inherited from Object
hand.getCard(0); // inherited from CardCollection| call | found in | how far up |
|---|---|---|
| display | Hand | the class itself |
| shuffle | CardCollection | one level |
| toString | Object | two levels |
| popCard | CardCollection | one level |
Java looks in the class first, then its superclass, then upward until it reaches Object. The first match wins — which is what makes overriding possible, as Lesson 11b's toString already showed without naming it.
Notation
Eight lines, and three of the calls in them are inherited. That is what a subclass looks like.
Annotate
getLabel() is inherited — it reads the private label that CardCollection's constructor set.size() is inherited, and it is why display never mentions an ArrayList.getCard(i) is inherited — the wrapper around cards.get(i).super.size() — inherited methods are called exactly like the class's own, because in every sense that matters they are.cards directly. It is private to CardCollection, so even a subclass must go through the public methods — which is why those wrappers exist.The last note is the subtle one. private keeps a subclass out too, which is why CardCollection's wrapper methods are not merely convenience — they are how a subclass reaches its own inherited data.
Worked example
One line of game code, and four classes involved. Follow it.
Deck deck = new Deck("Deck");
Hand hand = new Hand("Hand");
deck.deal(hand, 5);| step | method | defined in | operates on |
|---|---|---|---|
| 1 | deal(that, n) | CardCollection | deck (as this) |
| 2 | popCard() | CardCollection | deck |
| 3 | popCard(i) | CardCollection | deck |
| 4 | that.addCard(card) | CardCollection | hand |
| 5 | repeat 5 times | — | — |
deck.deal(...) finds no deal in Deck.
Why: Java looks up to CardCollection and finds it there.
Inside deal, this is the Deck.
Why: popCard() is called on it — the deck loses a card.
that is the Hand.
Why: Declared as CardCollection, holding a Hand — legal by substitution.
Five iterations, five cards moved.
Why: Deck 52 to 47; hand 0 to 5.
Verify: Print deck.size() and hand.size() before and after, and confirm 52/0 becomes 47/5.
Why: Every method in that trace lives in CardCollection, and neither subclass wrote a line of it. That is the return on the merge — and it is what makes the same call work for dealing, drawing and discarding in Lesson 14b.
Prediction
Hand's body contains a constructor and display.
Hand drawPile = new Hand("Draw Pile");
drawPile.shuffle();| class | has shuffle? |
|---|---|
| Hand | no |
| CardCollection | yes |
Predict first
What happens?
Correct: It works — shuffle is inherited from CardCollection
Why: Java looks in Hand, does not find shuffle, and looks in the superclass. Inherited methods are called exactly like a class's own. Note that Chapter 13's Pile had no shuffle at all — Hand gets it free, which is what reshuffle needs in Lesson 14b.
Concept
The card classes are now finished and game-agnostic. Everything specific to Crazy Eights goes in two new classes.
| class | knows about | reusable for another game? |
|---|---|---|
| Card | ranks and suits | yes |
| CardCollection | holding and moving cards | yes |
| Deck, Hand | being a deck, being a hand | yes |
| Player | the Crazy Eights strategy | no |
| Eights | the rules and the game loop | no |
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. 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.
Trap
private excludes subclasses too.
public class Hand extends CardCollection {
public void display() {
for (Card c : cards) { // compile error
System.out.println(c);
}
}
}| member | declared | visible in Hand? |
|---|---|---|
| cards | private in CardCollection | no |
| label | private in CardCollection | no |
| size() | public | yes |
| getCard(i) | public | yes |
The Hand object contains a cards list — every CardCollection does — but Hand's code cannot name it. Inheriting a member and being allowed to touch it are different things.
Go through the inherited public methods.
public void display() {
System.out.println(getLabel() + ": ");
for (int i = 0; i < size(); i++) {
System.out.println(getCard(i));
}
}| wanted | used instead |
|---|---|
| cards.size() | size() |
| cards.get(i) | getCard(i) |
| label | getLabel() |
This is why CardCollection's wrappers matter more than they look. They are not only for outside clients — they are the interface a subclass uses to reach data it technically has and cannot name.
Definition probe
Look in the class, then upward.
Sort into buckets
Sort each call on a Hand object.
Fill the middle
cards is private to CardCollection.
Fill in the blanks
public void display() size}(); i++) getCard}(i));
}
}
Why: A subclass inherits the private cards list as part of every object but cannot name it — private excludes subclasses too. The public wrappers are how Hand reaches data it technically has, which is a large part of why those wrappers were worth writing.
Explain it to yourself
Crazy Eights is the only game in the book.
Discussion prompt
Think Java says keeping the rules out of Deck and Hand is probably a good thing. What does that buy, concretely?
Hint: What would you have to do to write a second game?
Answer:
A second game reuses them unchanged. Poker, War, Go Fish — all need a deck, hands and piles, and none of them needs to know what an eight means.
If cardMatches lived in Card, every game would inherit Crazy Eights' rule, and a poker program would carry a method it must ignore.
The general principle: a class should know about its own domain and no further. Card knows about cards, CardCollection knows about holding them, and only Eights knows about eights — which is why Exercise 14.4 suggests an EightsCard subclass rather than changing Card.
Comparison
Fill the blanks.
Comparison matrix
| Chapter 13 | Chapter 14 | |
|---|---|---|
| how many collection classes | two — Deck and Pile | three — CardCollection, Deck, Hand |
| shared code | duplicated in both | written once in the superclass |
| storage | an array in Deck, an ArrayList in Pile | an ArrayList, once |
| can a Deck be shuffled | yes | yes |
| can a hand be shuffled | no — Pile had no shuffle | yes — inherited |
The last row is the unplanned benefit. Hand gains shuffle for free, and Lesson 14b's reshuffle needs exactly that — a feature the old Pile did not have.
Pattern
Extract a superclass when two classes are nearly the same, and let each subclass keep only its difference.
// 1. everything they share
public class CardCollection {
private String label;
private ArrayList<Card> cards;
public CardCollection(String label) { ... }
public void addCard(Card card) { ... }
public void deal(CardCollection that, int n) { ... }
}
// 2. each subclass adds only its difference
public class Deck extends CardCollection {
public Deck(String label) {
super(label); // superclass constructor FIRST
...build 52 cards...
}
}
public class Hand extends CardCollection {
public Hand(String label) { super(label); }
public void display() { ... }
}| rule | consequence |
|---|---|
| one superclass only | every chain ends at Object |
| constructors are not inherited | every subclass writes one, calling super |
| public members are inherited | call them unqualified |
| private excludes subclasses too | reach data through the public wrappers |
| a subclass is substitutable | a Hand goes where a CardCollection is expected |
Check
Work it out before you click.
public class Deck extends CardCollection {
public Deck(String label) {
super(label);
// ... build 52 cards with addCard ...
}
}| super(label) does | then |
|---|---|
| runs CardCollection's constructor | cards is an empty ArrayList |
Check your understanding
What happens if you delete the super(label) line?
Answer: A
Why: super invokes the CardCollection constructor, which initializes the attributes label and cards. Without it, cards is never created, so the first addCard has nothing to add to. The superclass's constructor must run first precisely because the subclass's own code depends on the state it sets up.
extends clause is what makes it a subclass; super is about construction.Check
Work it out before you click.
public void deal(CardCollection that, int n) { ... }
public void showIt(Hand h) { h.display(); }
Deck deck = new Deck("d");
Hand hand = new Hand("h");| call | ? |
|---|---|
| deal(hand, 5) | ? |
| showIt(deck) | ? |
Check your understanding
Which calls compile?
Answer: A
Why: A Hand is a CardCollection, so it can be passed wherever one is expected. But it doesn't work the other way around: not every CardCollection is a Hand, and a Deck is not a Hand at all — it has no display method for showIt to call. Every cat is a mammal; not every mammal is a cat.
Check
Work it out before you click.
public class CardCollection {
private ArrayList<Card> cards;
}
public class Hand extends CardCollection {
public void display() {
for (Card c : cards) { ... }
}
}| cards is | in Hand |
|---|---|
| private to CardCollection | ? |
Check your understanding
Does Hand's display compile?
Answer: A
Why: Every Hand object contains a cards list, because every CardCollection does — but Hand's code cannot name it, since private means within this class. Inheriting a member and being allowed to touch it are different things, which is a large part of why CardCollection's public wrappers were worth writing.
Real world
The extends keyword makes a promise about what your class is, and every method expecting the superclass will believe it.
Discussion prompt
Where else does the is a test decide a design, inside or outside programming — and what goes wrong when the relationship is asserted for convenience?
Hint: A square and a rectangle.
Answer:
The classic case is Square extends Rectangle. Geometrically a square is a rectangle, so it looks right — until a method calls setWidth and setHeight independently and the square stops being one.
The test is not whether the sentence sounds true, but whether every method of the superclass still makes sense on the subclass. If any does not, the relationship is not IS-A.
Outside code the same trap appears whenever a category is inherited for convenience — a form that treats one thing as another because the paperwork already exists. Composition is the honest answer whenever the sentence is really has a, which is why Player has a Hand rather than being one.
Commit first
Commit to an answer and to your confidence.
Predict first
Which of these is NOT inherited by a subclass?
Correct: Constructors
Why: Think Java states it directly: constructors are not inherited, but all other public attributes and methods are. That is why Deck and Hand each write their own constructor, and why each begins with super(label) — the superclass's constructor still has to run, it just has to be invoked explicitly. The fourth option is inherited in the sense that matters: a subclass object satisfies any parameter declared as the superclass, which is what makes one deal method serve every collection in the game.
Explain it
Two minutes, out loud.
Discussion prompt
A classmate asks why the book bothered writing CardCollection when Deck and Pile already worked. Give them the argument, and name one thing the new design can do that the old one could not.
Hint: Count the copies of addCard. And ask whether a hand could be shuffled.
Answer:
Deck and Pile were essentially two versions of the same code — add, remove, isEmpty, all written twice, so every bug had two homes and every change had to be made in both.
CardCollection holds what they share, and each subclass keeps only its difference: Deck adds a constructor that builds 52 cards, Hand adds a display method. That is about twelve lines between them.
And the new design can shuffle a hand — Pile never had a shuffle, and Hand inherits one. A shared superclass spreads improvements automatically, which is the benefit that keeps paying after the duplication is gone.
Exit ticket
One question before you close the deck.
Predict first
Why can a Hand be passed to a method that expects a CardCollection?
Correct: Because Hand extends CardCollection, so a Hand object is also a CardCollection object
Why: Inheritance means an object belongs to more than one type at once: a Hand is a Hand, a CardCollection, and an Object. Nothing is converted — the object genuinely has every method the parameter promises. The reverse fails for the same reason: not every CardCollection is a Hand, so a method expecting a Hand may call display, which a plain CardCollection does not have. Every cat is a mammal; not every mammal is a cat.
Connect it up
One page, from memory.
Draw it
Draw the four-class hierarchy — Object, CardCollection, Deck, Hand — with hollow inheritance arrows, and write in each box only what that class actually declares. Beside it, list the four rules of extends: one superclass, Object at the top, constructors not inherited, everything else public inherited. Then write deck.deal(hand, 5) and trace which class each method in it comes from. Finish with the cat–mammal–animal sentence and the two parameter rules it settles.
Recap
Three sections that turn duplicated code into a hierarchy, and then show what the hierarchy buys.
| if you remember one thing | it is this |
|---|---|
| about when to extend | say is a out loud; if it is has a, use a field |
| about super | the superclass constructor runs first, always |
| about substitution | specific satisfies general, never the reverse |
super(...) invokes the superclass's.Want this taught 1-on-1? Alexander tutors Java — $55/session, free consultation.