Card Objects, Class Variables, and compareTo

Encoding suits and ranks as integers so cards can be compared, decoding them back into words with an array of Strings, class variables shared by every object, a compareTo that imposes an order on a partially ordered set, and the decision to make cards immutable. Follows Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.1-12.5, pp. 201-208, cross-referenced against Java SE 21 API — java.lang.Comparable.

Subject: Java · 65 slides · code lesson

Open the interactive version of this deck

What this lesson covers

The lesson, slide by slide

1. Card Objects, Class Variables, and compareTo

Title

Think Java 2e · Chapter 12 · Arrays of Objects

Sections 12.1-12.5 · pp. 201-208

2. What you will be able to do

Objectives

This lesson follows Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.1-12.5, pp. 201-208. Everything on these slides can be checked against those pages.

1. Explain what it means to encode a set of values, and why integers were chosen over Strings.

2. Use an array of Strings to decode an integer back into a word.

3. Distinguish a class variable from an instance variable, and say what static and final each mean.

4. Write a compareTo method returning a negative number, zero or a positive number.

5. Explain what totally ordered, partially ordered and unordered mean.

6. Make a class immutable by omitting setters and declaring the instance variables final.

3. Retrieve before you read

Warm-up

Two techniques from earlier chapters are about to be combined.

Discussion prompt

From Lesson 7b: how did the doubloon program turn a letter into an array index? And from Lesson 11b: what does compareTo return, and what do you compare its result against?

Hint: One is subtraction; one is a sign.

Answer:

letter - 'a' mapped a letter to 0 through 25 so it could index an array of counters. compareTo returns a negative number, zero or a positive number, and you compare that result against zero rather than against a specific value.

This chapter uses both: an integer encoding so cards can be indexed and compared, and a compareTo of your own so they can be sorted. In this chapter we define a Card class, and the next two build a Deck and then a game of Crazy Eights on top of it.

4. Choosing a representation

Concept

A standard deck has 52 cards, each belonging to one of four suits and one of thirteen ranks. It is clear what the instance variables should be — rank and suit — and not obvious what types they should be. That choice decides everything else in the chapter.

Figure (svg): Two boxes comparing storing suits as Strings against storing them as integers, with the comparison problem noted

Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.1-12.5, pp. 201-208 — Chapter 12 opens on printed page 201.

5. Encoding ranks and suits

Section

Section 12.1

6. A mapping between numbers and things

Concept

One possibility is a String containing "Spade" for suits and "Queen" for ranks. A problem with that choice is that it would not be easy to compare cards to see which had a higher rank or suit. The alternative is to encode them as integers.

// suits              // ranks
// Clubs    -> 0      // Ace  -> 1
// Diamonds -> 1      // 2-10 -> 2 to 10
// Hearts   -> 2      // Jack -> 11
// Spades   -> 3      // Queen -> 12
//                    // King  -> 13
cardranksuit
Ace of Clubs10
3 of Clubs30
Jack of Diamonds111
King of Spades133

encode — To represent one set of values using another set of values by constructing a mapping between them.

By encode we do not mean to encrypt or translate into a secret code. We mean to define a mapping between a sequence of numbers and the things we want to represent. The numeric ranks 2 through 10 map to themselves, which keeps the mapping easy to remember.

7. The mapping is design, not code

Notation

Think Java uses a mathematical arrow for the mapping deliberately, and the reason is worth noticing.

Annotate

  • The arrows are not Java. Nowhere in the program does a line say Clubs is 0 — the mapping lives in the programmer's head and in the comments.
  • That is a real risk. A decision that exists only as a convention can be forgotten or contradicted, which is exactly what Section 12.3's SUITS array partly fixes by writing the order down.
  • Ace maps to 1, not 0. So rank 0 is unused, which is why the ranks array will need a placeholder at index 0.
  • The numeric ranks map to themselves, so rank 7 is the 7. Only the four face cards need remembering, which is a deliberate choice to keep the encoding legible.
  • Clubs to Spades is the order a new deck comes in — which the chapter uses later to justify making suit more important than rank.

An encoding is a design decision that the compiler cannot check. Choosing one that is easy to remember, and writing it down, is most of the work.

8. Why integers make comparison possible

Worked example

The whole argument for the integer encoding is one thing Strings cannot do. Make it concrete.

// with Strings:
"Queen" < "Jack"          // does not compile - < is for primitives
"Queen".compareTo("Jack") // positive - but that is ALPHABETICAL order

// with integers:
12 > 11                   // true, and it means Queen beats Jack
comparisonwith Stringswith integers
is Queen higher than Jack?alphabetical order says J before Q — coincidentally right12 > 11 — right by design
is 10 higher than 9?"10" < "9" alphabetically — wrong10 > 9 — right
is Ace higher than King?depends on the game, and alphabetical says A first1 < 13 — a decision you make

Notice that < does not work on objects at all.

Why: The comparison operators are for primitive types, as Lesson 9b showed for BigInteger.

Notice that String's compareTo gives alphabetical order.

Why: Which is not rank order — "10" sorts before "9" because '1' comes before '9'.

Notice the integers just work.

Why: this.rank < that.rank is an ordinary comparison of two ints, and it means exactly what you want.

Notice the cost.

Why: 12 does not read as Queen, so displaying a card now needs decoding — which is the next section.

Verify: Convince yourself that "10".compareTo("9") is negative, so alphabetically a 10 sorts before a 9.

Why: That single fact settles the design. Strings are easy to display and hard to compare; integers are easy to compare and need decoding to display — and comparison is the harder problem, so the encoding wins.

9. What card is new Card(12, 2)?

Prediction

Apply the two mappings.

// ranks: Ace 1, 2-10 themselves, Jack 11, Queen 12, King 13
// suits: Clubs 0, Diamonds 1, Hearts 2, Spades 3
valuepositionmeans
12rankQueen
2suitHearts

Predict first

Which card is it?

  • Queen of Hearts
  • 2 of Queens
  • Hearts of 12
  • Jack of Hearts

Correct: Queen of Hearts

Why: The constructor takes rank first and suit second, so 12 is the rank — Queen — and 2 is the suit — Hearts. Note how much you had to know to answer: the parameter order and both mappings, none of which the call itself states. That is the encoding's cost.

10. The Card class so far

Concept

With the encoding decided, the class definition is the Chapter 11 pattern applied to two integers.

public class Card {
    private int rank;
    private int suit;

    public Card(int rank, int suit) {
        this.rank = rank;
        this.suit = suit;
    }
}
partdecision it records
private int rank, suitthe data, encoded as integers, hidden from clients
Card(int rank, int suit)how a Card is created
this.rank = rankthe shadowing pattern from Lesson 11a
no getters yetSection 12.5 decides what to expose

The instance variables are private: we can access them from inside this class, but not from other classes. new Card(3, 0) produces a reference to a Card representing the 3 of Clubs — and a reader who does not know the encoding cannot tell that from the call, which is a real weakness this chapter acknowledges.

11. An encoding nobody wrote down

Trap

The trap

new Card(3, 0) says nothing about what it means.

Card c1 = new Card(3, 0);      // 3 of Clubs? or Clubs of 3?
Card c2 = new Card(0, 3);      // and this?
Card c3 = new Card(1, 2);      // Ace of Hearts, or 2 of... something?
problemconsequence
the parameter order is invisible at the callrank and suit are easy to swap
both are intsthe compiler cannot catch a swap
the mapping is only a conventiona reader has to look it up

Two int parameters of the same type means swapping them compiles perfectly. This is the cost of the encoding, and it is real.

The fix

Name the values, so the call site says what it means.

// with the class variables from Section 12.3:
Card c = new Card(3, Card.CLUBS);

// or at minimum, a comment recording the mapping:
// suits: 0 Clubs, 1 Diamonds, 2 Hearts, 3 Spades
improvementwhat it buys
named constantsthe call reads correctly and a swap becomes visible
the SUITS arraythe order is written down in code, not just in comments
a commentbetter than nothing, and cannot be checked

This is Lesson 3a's magic-number argument in a new setting. A bare 0 in a call is exactly the kind of literal that should have a name — and the next section gives these ones names for a different reason that happens to help here too.

12. Which representation for which job?

Definition probe

Strings display well; integers compare well.

Sort into buckets

Sort each task by the representation that makes it easy.

integers
tell which of two ranks is higher; use the suit as an array index; sort a hand of cards
Strings
display a card to a player
int
Integers can be compared with < and >, and used directly as array indexes — which is exactly what the doubloon program needed in Lesson 7b.
str
Text reads naturally for a human. This is the one thing the integer encoding cannot do, and it is why toString exists.

13. Why is rank 0 unused?

Prediction

Ace maps to 1, not 0.

// Ace -> 1, King -> 13
// so valid ranks are 1 to 13
rank valuecard
0none — unused
1Ace
13King

Predict first

What consequence does starting at 1 have for a decoding array?

  • The array needs a placeholder at index 0, which is never used
  • The array must have 13 elements
  • The array cannot be used at all
  • Ranks must be shifted down by one before indexing

Correct: The array needs a placeholder at index 0, which is never used

Why: If rank 1 is to index directly into the array, index 0 has to exist and hold something — so the array has 14 elements with a placeholder first. The alternative would be subtracting 1 before every lookup, which is an off-by-one waiting to happen; the placeholder trades one wasted element for simpler code.

14. Is the encoding arbitrary?

Socratic

Clubs could have been 3 and Spades 0.

Discussion prompt

The mapping Clubs to 0 through Spades to 3 is a choice. What makes this particular choice better than a random one — and what would break if you changed it later?

Hint: What order does a new deck come in?

Answer:

It matches the order a new deck comes in — Clubs, Diamonds, Hearts, Spades — so a sorted array of cards looks like a new deck, and the ordering feels natural rather than invented.

Changing it later would break anything that depends on the order: compareTo, any code that sorts, and the SUITS array whose element order encodes the same decision.

An encoding is a contract, even though nothing in the language enforces it. That is why writing it into a class variable — as the next section does — is worth more than leaving it in a comment: at least then there is one place it lives.

15. Decoding for toString

Section

Section 12.2

16. An array of Strings turns a number back into a word

Concept

When you create a new class, the first step is to declare the instance variables and write constructors. A good next step is to write toString, which is useful for debugging and incremental development. To display a Card readably we have to decode the integers back into words.

String[] suits = {"Clubs", "Diamonds", "Hearts", "Spades"};

String[] ranks = {null, "Ace", "2", "3", "4", "5", "6",
                  "7", "8", "9", "10", "Jack", "Queen", "King"};

String s = ranks[this.rank] + " of " + suits[this.suit];
expressionfor rank 11, suit 1
ranks[this.rank]ranks[11] is "Jack"
suits[this.suit]suits[1] is "Diamonds"
the whole expression"Jack of Diamonds"

ranks[this.rank] means use the instance variable rank from this object as an index into the array ranks. That is Lesson 7b's value-as-index trick, used to decode rather than to count.

17. An array of references to Strings

Picture it

Each element of the array is a reference to a String object — which is exactly what Lesson 9a's picture of an object variable predicted.

Figure (svg): An array of four elements each holding the name of a suit, with indexes 0 to 3 underneath

The array's index order encodes the mapping: Clubs is at 0 because Clubs maps to 0. So the array is not just a lookup table — it is the one place in the program where the design decision is written down in code.

18. The null placeholder

Worked example

The ranks array has fourteen elements for thirteen ranks. The extra one is deliberate and its value is chosen carefully.

String[] ranks = {null, "Ace", "2", "3", "4", "5", "6",
                  "7", "8", "9", "10", "Jack", "Queen", "King"};
indexelementused?
0nullnever — rank 0 does not exist
1"Ace"yes
11"Jack"yes
13"King"yes
length14one more than the number of ranks

Count the elements.

Why: Fourteen, for thirteen ranks — because valid ranks run 1 to 13 and index 0 must exist.

Notice why the placeholder is null.

Why: The zeroth element should never be used, and null indicates an unused element rather than pretending to be a rank.

Notice what null buys.

Why: If a bug ever produces rank 0, the output is the word null — visible and wrong — rather than a plausible-looking card.

Notice the alternative.

Why: Subtracting 1 before every lookup would avoid the wasted element and introduce an off-by-one everywhere.

Verify: Create new Card(11, 1) and print it; expect Jack of Diamonds.

Why: Then imagine a bug that produced rank 0. ranks[0] is null, so the output would be null of Clubs — obviously wrong at a glance. Choosing a placeholder that cannot be mistaken for real data is a small decision that pays off during debugging.

19. What does this print?

Prediction

Decode both integers.

Card card = new Card(11, 1);
System.out.println(card);
lookupresult
ranks[11]"Jack"
suits[1]"Diamonds"

Predict first

What is displayed?

  • Jack of Diamonds
  • 11 of 1
  • Card@1a2b3c4d
  • Queen of Diamonds

Correct: Jack of Diamonds

Why: println calls toString automatically, and toString uses the two integers as indexes into the decoding arrays. Without a toString the output would be the type and address instead — which is exactly why the book recommends writing one immediately after the constructor.

20. toString for Card

Concept

Wrapping the decoding in a toString gives the class the readable output Lesson 11b established, and it is the first thing to write after the constructor.

public String toString() {
    String[] ranks = {null, "Ace", "2", "3", "4", "5", "6",
                      "7", "8", "9", "10", "Jack", "Queen", "King"};
    String[] suits = {"Clubs", "Diamonds", "Hearts", "Spades"};

    String s = ranks[this.rank] + " of " + suits[this.suit];
    return s;
}
cardranksuittoString
new Card(11, 1)111Jack of Diamonds
new Card(1, 0)10Ace of Clubs
new Card(13, 3)133King of Spades

When we display a card, println automatically calls toString — the mechanism from Lesson 11b. Writing it early is what makes the rest of the chapter debuggable: without it, every Card prints as Card@1a2b3c4d and you cannot see what your code is doing.

21. Building the arrays on every call

Trap

The trap

Two arrays created and discarded every time a card is printed.

public String toString() {
    String[] ranks = { ... 14 strings ... };
    String[] suits = { ... 4 strings ... };
    return ranks[this.rank] + " of " + suits[this.suit];
}
printingarrays created
one card2
a 52-card deck104
a deck a thousand times104,000

They are local variables, so each call allocates them and each return abandons them — which is Lesson 10b's garbage, generated for nothing. It works, and it is wasteful.

The fix

Make them class variables, allocated once for the whole program.

public static final String[] RANKS = { ... };
public static final String[] SUITS = { ... };

public String toString() {
    return RANKS[this.rank] + " of " + SUITS[this.suit];
}
local arraysclass variables
createdon every callonce, when the program begins
garbage producedtwo arrays per callnone
usable from other methodsnoyes

One advantage of defining SUITS and RANKS as class variables is that they do not need to be created and garbage-collected every time toString is called. They may also be needed in other methods and classes, so it helps to make them available everywhere — which is the next idea.

22. How many elements does the ranks array have?

Prediction

Thirteen ranks, and a placeholder.

String[] ranks = {null, "Ace", "2", ..., "King"};
indexholds
0null
1 to 13the thirteen rank names

Predict first

What is ranks.length?

  • 14
  • 13
  • 12
  • 52

Correct: 14

Why: Thirteen rank names plus a placeholder at index 0, because ranks are numbered from 1 and the array must have an element 0 for direct indexing to work. The wasted element buys code with no subtraction in it, which is a good trade.

23. Decode a card

Fill the middle

Use the instance variables as indexes.

Fill in the blanks

String s = ranks[this.rank] + " of " + suits[this.suit];

Why: this.rank is the object's own rank, used as an index into the ranks array — the value-as-index technique from Lesson 7b. Without this a bare rank would work here too, since there is no shadowing in this method, but writing it makes clear the value comes from the object rather than from a local.

24. Why write toString so early?

Explain it to yourself

Before compareTo, before getters, before anything else.

Discussion prompt

Think Java says a good next step after the constructor is toString. Why before the methods that do the real work?

Hint: What can you see without it?

Answer:

Because without it you cannot see what your code is doing. Every Card prints as Card@1a2b3c4d, so any bug in a later method has to be diagnosed blind.

It is useful for debugging and incremental development — Lesson 4b's method, applied to a class. The first thing worth building is the thing that lets you check everything built afterwards.

The general habit: build the tool that makes the work visible before doing the work. It is the same reason the histogram in Lesson 7b was printed before being analysed.

25. Class variables

Section

Section 12.3

26. One copy, shared by every object

Concept

You have seen local variables, declared inside a method, and instance variables, declared in a class definition. Now for class variables: they are also declared in the class, before the methods, but they are identified by the keyword static — and they are shared across all instances.

public class Card {

    public static final String[] RANKS = {
        null, "Ace", "2", "3", "4", "5", "6", "7",
        "8", "9", "10", "Jack", "Queen", "King"};

    public static final String[] SUITS = {
        "Clubs", "Diamonds", "Hearts", "Spades"};

    // instance variables and constructors go here

    public String toString() {
        return RANKS[this.rank] + " of " + SUITS[this.suit];
    }
}
instance variableclass variable
keywordnonestatic
how many copiesone per objectone, ever
allocatedwhen an object is createdwhen the program begins
deletedwhen the object is garbage-collectedwhen the program ends

class variable — A variable declared within a class as static. There is only one copy, no matter how many objects there are.

Class variables are allocated when the program begins and persist until the program ends. In contrast, instance variables like rank and suit are allocated when the program creates new objects and deleted when the object is garbage-collected.

27. static and final are separate decisions

Notation

public static final String[] RANKS has three modifiers, and each one means something different.

Annotate

  • static means the variable is shared. One copy exists regardless of how many Card objects there are — and it exists even if there are none.
  • final means the variable is constant. Note the careful wording: in this case it is the reference that is constant, so RANKS cannot be made to point at a different array.
  • Whether a variable is static or final involves two separate considerations. A variable can be one, the other, both, or neither.
  • Naming static final variables with capital letters is a common convention that makes their role easy to recognise — the same convention as Lesson 3a's CM_PER_INCH.
  • In toString we refer to SUITS and RANKS as if they were local variables, with no this and no class name, but the capitals tell you they are class variables.

The second note is the subtle one. final on an array reference stops you reassigning the reference — it does not stop anyone changing an element. That is a real gap, and the next hazard is about it.

28. Choosing between the three kinds of variable

Worked example

You now have three places to declare something. The deciding question is how many copies should exist and how long each should live.

public class Card {

    public static final String[] SUITS = { ... };   // one, for the class

    private int rank;                                // one per Card
    private int suit;

    public String toString() {
        String s = RANKS[this.rank];                 // one per call
        ...
    }
}
variablehow many copieslifetime
SUITSone, for the whole programprogram start to end
rankone per Card objectthe object's lifetime
sone per call to toStringthe method call

Ask whether every object needs its own.

Why: Each card has its own rank, so rank is an instance variable.

Ask whether one copy would do for all of them.

Why: Every card decodes with the same SUITS array, so one shared copy is right.

Ask whether it is needed only during one call.

Why: A temporary string is a local variable.

Then decide on final.

Why: SUITS should never be reassigned, so it is final as well as static.

Verify: Create fifty-two Cards and confirm the program has fifty-two ranks, fifty-two suits, and exactly one SUITS array.

Why: Class variables are often used to store constant values that are needed in several places — which is precisely what a decoding table is. The Deck class in Chapter 13 will use SUITS and RANKS too, and it can, because they are public.

29. Which kind of variable?

Definition probe

How many copies should exist?

Sort into buckets

Sort each variable in the Card class.

class variable — static
SUITS, the decoding array; RANKS, the other decoding array
instance variable
rank, a card's own rank; suit, a card's own suit
local variable
s, a temporary inside toString
cls
Shared by every object and needed even before any exist. One copy, allocated when the program begins.
inst
Each object needs its own value, so one copy per object, created with it and destroyed with it.
loc
Needed only during a single method call, so it is created on invocation and discarded on return.

30. Where you have already used class variables

Concept

Every constant and method you have called on a class rather than an object was static. Naming the concept explains several things at once.

you wrotewhich isbecause
Integer.MAX_VALUEa class variablethere is one, shared, not one per Integer
Math.PIa class variablepi does not vary per object
Math.sqrt(x)a static methodno object owns square-rooting
Integer.parseInt(s)a static methodit makes an int rather than using one
Card.SUITSa class variableone decoding table for all cards

The pattern is consistent: static members are reached through the class name, because they do not belong to any object. That is also why main is static — Lesson 11b's point — and why you cannot use this inside one.

31. Thinking final protects the array's contents

Trap

The trap

final freezes the reference, not the elements.

public static final String[] SUITS = {"Clubs", ...};

SUITS = new String[4];      // error - the reference is final
SUITS[0] = "Wands";         // legal! the elements are not
attemptlegal?why
reassign SUITSnothe reference is final
change SUITS[0]yesfinal says nothing about the elements
consequence—every card's suit name changes at once

Because SUITS is public and static, any class can change an element, and every Card in the program immediately decodes differently. This is Lesson 10a's aliasing at the scale of a whole program.

The fix

Know what final promises, and decide whether public is worth the risk.

// final: this reference will always point at this array
public static final String[] SUITS = { ... };

// if the contents must be protected, do not expose the array:
private static final String[] SUITS = { ... };
public static String suitName(int suit) {
    return SUITS[suit];
}
designclients can readclients can modify
public static final arrayyesyes — the elements
private array plus a methodyesno

The book makes SUITS public because Chapter 13's Deck class needs it, which is a reasonable trade for a textbook program. The point is knowing that it is a trade — final on an array is a weaker promise than it looks, and Lesson 11a's argument for private applies here too.

32. How many SUITS arrays exist?

Prediction

Fifty-two cards are created.

public static final String[] SUITS = { ... };
// then 52 Card objects are created
whathow many
Card objects52
rank variables52
SUITS arrays1

Predict first

How many SUITS arrays are there?

  • One, shared by all of them
  • 52 — one per card
  • 53
  • None until a card is created

Correct: One, shared by all of them

Why: A class variable is shared across all instances — there is only one copy, no matter how many objects exist. It is allocated when the program begins, so it exists even before the first Card is created, which is exactly why it can be used from a static context.

33. Match the modifier to its meaning

Matching

Three modifiers, three separate decisions.

Match the pairs

  • a. public
  • b. static
  • c. final
  • d. private
  • r1. usable from other classes
  • r2. shared — one copy for the class
  • r3. constant — cannot be reassigned
  • r4. usable only inside this class

Why: The three are independent: a variable can be public without being static, static without being final, and so on. Think Java is explicit that static means shared and final means constant are two separate considerations that happen to be combined for a decoding table.

34. Can a static method use an instance variable?

Edge cases

Push on what shared means.

Discussion prompt

SUITS is static and rank is not. Could a static method use rank directly? Reason from what static means before answering.

Hint: Which object's rank would it be?

Answer:

No. A static method belongs to the class and may be called when no objects exist at all — so there is no particular object whose rank it could mean.

That is exactly the error from Lesson 11b: non-static variable cannot be referenced from a static context. It is the same reason main cannot use instance variables directly.

The reverse is fine: an instance method can use a class variable, because a shared thing is available to everyone. toString uses SUITS freely — which is why the arrow only points one way.

35. The compareTo method

Section

Section 12.4

36. Comparing objects needs a method

Concept

For primitive types we can use < and > to compare values. But these operators do not work for object types. For Strings, Java provides compareTo; for classes we define, we can write our own — just as we did for equals.

public boolean equals(Card that) {
    return this.rank == that.rank
        && this.suit == that.suit;
}
comparisonfor primitivesfor objects
are these equal?==equals
is this one bigger?< and >compareTo
who decides what bigger means?the languagethe class

This is Lesson 11b's equals, now for Card — and both instance variables are ints, so both are compared with ==. There is no double here, so no tolerance is needed.

37. Three kinds of order

Notation

Before writing compareTo, it is worth asking whether cards can be ordered at all. The answer is not naturally — which is why the method has to make a decision.

Annotate

  • Integers and strings are totally ordered — you can compare any two and tell which is bigger.
  • In Java the boolean type is unordered. Writing true < false is a compile error, because there is no sensible answer.
  • The set of playing cards is partially ordered. The 3 of Clubs is higher than the 2 of Clubs, and the 3 of Diamonds is higher than the 3 of Clubs — but which is better, the 3 of Clubs or the 2 of Diamonds?
  • One has a higher rank and the other a higher suit, so the comparison has no natural answer. To make cards comparable we have to decide which is more important.
  • The choice is arbitrary and might be different for different games. Think Java picks suit, because a new deck comes sorted with all the Clubs together.

That is worth noticing as a general point: writing compareTo for a partially ordered type means imposing an order rather than discovering one, and the class's author has to choose.

38. Writing compareTo

Worked example

With suit decided as more important, the method compares suits first and falls back on ranks.

public int compareTo(Card that) {
    if (this.suit < that.suit) {
        return -1;
    }
    if (this.suit > that.suit) {
        return 1;
    }
    if (this.rank < that.rank) {
        return -1;
    }
    if (this.rank > that.rank) {
        return 1;
    }
    return 0;
}
thisthatcompared onreturns
3 of Clubs (3,0)2 of Diamonds (2,1)suit: 0 < 1-1
3 of Clubs (3,0)2 of Clubs (2,0)suits equal, rank: 3 > 21
3 of Clubs (3,0)3 of Clubs (3,0)both equal0

Compare the suits first.

Why: If this suit is lower, return -1; if higher, return 1. That is the decision that suit matters more.

If the suits are the same, compare the ranks.

Why: The same two tests, on the other instance variable.

If the ranks are also the same, return 0.

Why: compareTo returns -1 if this is a lower card, +1 if higher, and 0 if this and that are equivalent.

Note that reaching the last line means everything matched.

Why: Which is the same shape as array11 returning 0 only after the loop finished, in Lesson 8b.

Verify: Compare the 3 of Clubs with the 2 of Diamonds and expect -1 — the Club is 'lower' because Clubs sort first.

Why: That answer is not a fact about cards; it is the consequence of a decision this method made. A different game could reasonably return the other answer, which is why compareTo belongs to the class rather than to the language.

39. What does compareTo return?

Prediction

Suit is compared first.

Card a = new Card(2, 3);    // 2 of Spades
Card b = new Card(13, 0);   // King of Clubs
a.compareTo(b)
comparisonvaluesresult
suits3 against 0this is higher
ranksnever reached—

Predict first

What is returned?

  • 1 — Spades outranks Clubs, and suit is compared first
  • -1 — a 2 is lower than a King
  • 0
  • 11 — the rank difference

Correct: 1 — Spades outranks Clubs, and suit is compared first

Why: The method compares suits before ranks, and Spades (3) is higher than Clubs (0), so it returns 1 immediately and never looks at the ranks. That is the arbitrary decision the class made — a game where rank mattered more would return -1 for the same pair.

40. Why the sign rather than the value

Concept

compareTo returns -1, 0 or 1 here, but callers should test the sign rather than the exact number — as Lesson 6b's String comparison already showed.

// correct
if (a.compareTo(b) < 0) { ... }

// fragile
if (a.compareTo(b) == -1) { ... }
classwhat compareTo returns
Cardexactly -1, 0 or 1
Stringthe difference between the first differing characters
Integer-1, 0 or 1
what callers should rely ononly the sign

Lesson 6b's "Alan Turing".compareTo("Ada Lovelace") returned 8, not 1 — because String's version returns a character difference. A caller testing == 1 would work for Card and fail for String, which is why the contract is about the sign only.

41. Comparing objects with < and >

Trap

The trap

The relational operators are for primitives.

Card a = new Card(3, 0);
Card b = new Card(2, 1);

if (a < b) { ... }          // does not compile
if (a > b) { ... }          // does not compile
operatoron intson objects
<compares valuescompile error
>compares valuescompile error
==compares valuescompiles — compares references

The third row is the dangerous one and you have met it three times now: == does compile on objects and asks the wrong question. < and > at least fail loudly.

The fix

compareTo, compared against zero.

if (a.compareTo(b) < 0) {
    System.out.println("a is lower");
} else if (a.compareTo(b) > 0) {
    System.out.println("a is higher");
} else {
    System.out.println("equivalent");
}
wantwrite
a < ba.compareTo(b) < 0
a > ba.compareTo(b) > 0
a equals ba.equals(b), or compareTo(b) == 0

The pattern is identical to Lesson 6b's String comparison and Lesson 9b's BigInteger: objects compare with a method whose result you compare against zero. Three classes, one idiom.

42. Totally, partially or unordered?

Definition probe

Can any two values be compared?

Sort into buckets

Sort each type.

totally ordered
int; String
unordered
boolean
partially ordered
playing cards
tot
Any two values can be compared and one is definitively bigger — which is why < and > work for ints and why String provides compareTo.
un
There is no meaningful way to say one value is bigger. Java rejects true < false at compile time.
part
Some pairs compare naturally and others do not — the 3 of Clubs against the 2 of Diamonds has no natural answer, so the class must decide.

43. Compare against zero

Fill the middle

Test whether a comes before b.

Fill in the blanks

if (a.compareTo(b) < 0) ___

Why: compareTo returns a negative number when the object it is invoked on comes first, so the test is against zero rather than against -1. Testing == -1 happens to work for Card and fails for String, whose compareTo returns a character difference — which is why the sign is the whole contract.

44. Could compareTo rank Aces high?

Counterexample

The encoding puts Ace at 1, the lowest rank.

Discussion prompt

In many games an Ace beats a King. With Ace encoded as 1 and King as 13, compareTo says the Ace is lower. Is the encoding wrong — and how would you handle a game where Aces are high?

Hint: Does the encoding have to match the game's ordering?

Answer:

The encoding is not wrong; it is just one mapping. The ordering is a separate decision from the encoding, and it lives in compareTo rather than in the numbers.

For Aces high you could change compareTo to treat rank 1 as if it were 14 — without touching the encoding, the constructor, or toString. Only the comparison changes.

That separation is the point. The encoding says what a card is; compareTo says how cards are ordered, and Think Java notes explicitly that the choice might be different for different games. Keeping the two apart is what makes the class reusable across Chapters 13 and 14.

45. Cards are immutable

Section

Section 12.5

46. Getters, and the decision not to write setters

Concept

The instance variables of Card are private, so they cannot be accessed from other classes. We provide getters to let other classes read the rank and suit — and then face a design decision about setters.

public int getRank() {
    return this.rank;
}

public int getSuit() {
    return this.suit;
}
provideconsequence
getters onlyclients can read a card but not change it
getters and setterscards become mutable — one card can be turned into another
neitherclients cannot see a card's rank at all

Whether or not to provide setters is a design decision. If we did, cards would be mutable, so you could transform one card into another — that is probably not a feature we want, and in general mutable objects are more error-prone.

47. Two ways to make a class immutable

Picture it

Omitting setters is enough. Declaring the instance variables final is enough and enforced.

Figure (svg): Two panels comparing immutability by convention with immutability enforced by the final keyword

That is easy enough, but it is not foolproof, because a fool might come along later and add a modifier. Declaring the instance variables final prevents that possibility — and the fool in question is usually you, six months later.

48. Making Card immutable

Worked example

One keyword per instance variable, and the class becomes impossible to modify rather than merely unmodified.

public class Card {
    private final int rank;
    private final int suit;

    public Card(int rank, int suit) {
        this.rank = rank;      // legal - initialising a final variable
        this.suit = suit;
    }

    public void setRank(int rank) {
        this.rank = rank;      // compile error
    }
}
assignmentwherelegal?
this.rank = rank;in the constructoryes — the one permitted initialisation
this.rank = rank;in a setterno — cannot assign a final variable
reading this.rankanywhere in the classyes

Add final to each instance variable.

Why: private final int rank;

Initialise them in the constructor.

Why: You can initialise these variables inside a constructor, which is the one place assignment is allowed.

Try to write a setter.

Why: If someone writes a method that tries to modify them, they will get a compiler error.

Notice what this buys.

Why: This kind of safeguard helps prevent future mistakes and hours of debugging.

Verify: Add a setRank method and confirm the class no longer compiles.

Why: That failure is the feature. It is Lesson 5a's argument for always writing braces, applied to class design: prefer a rule the compiler enforces over a convention you have to remember.

49. What does final on an instance variable prevent?

Prediction

It permits one assignment.

private final int rank;

public Card(int rank) {
    this.rank = rank;        // ?
}

public void setRank(int rank) {
    this.rank = rank;        // ?
}
assignmentlegal?
in the constructoryes
in a setterno

Predict first

Which assignment is a compile error?

  • The one in the setter — a final variable can only be initialised once
  • The one in the constructor
  • Both
  • Neither — final only applies to class variables

Correct: The one in the setter — a final variable can only be initialised once

Why: A final instance variable may be initialised in the constructor and never assigned again, so the constructor's assignment is exactly the permitted one and the setter's is rejected. That is what makes the class impossible to modify rather than merely unmodified.

50. Why immutable is the safer default

Concept

Chapter 10 argued that neither design is always better. For a Card, the arguments point one way.

if cards were mutablebecause they are immutable
a card in a hand could be silently changeda card is a card, permanently
passing one to a method would be a riskyou can pass one anywhere safely
two references to one card would be a hazardaliasing is harmless
a shuffled deck could be corrupted by a bug elsewhereonly the array of references can change

The last row matters for Chapter 13: a Deck will shuffle by rearranging references, and the Cards themselves never change. That is only safe because Card is immutable — which is Lesson 9a's argument that immutability makes sharing free, now being relied on rather than merely described.

51. Relying on the absence of setters

Trap

The trap

Immutable by convention — until someone adds a method.

public class Card {
    private int rank;         // not final
    private int suit;

    // no setters... today
}

// six months later, someone adds:
public void setRank(int rank) {
    this.rank = rank;         // compiles fine
}
whenis Card immutable?
todayyes, in practice
after the setter is addedno
does anything warn you?no

Nothing records the decision, so nothing defends it. Every piece of code that relied on cards being immutable — including Chapter 13's shuffling — is now built on an assumption that has quietly stopped being true.

The fix

final records the decision and enforces it.

public class Card {
    private final int rank;
    private final int suit;
    ...
}
// now the setter cannot be written at all
approachrecords the intent?enforced?
no settersimplicitlyno
a comment saying immutableyesno
final instance variablesyesyes

A safeguard that the compiler checks beats one that a reader has to notice. That is the same reasoning as final for constants in Lesson 3a — and the same reasoning as always writing braces.

52. Which design for which class?

Definition probe

Chapter 10's question, applied to specific types.

Sort into buckets

Sort each class by the design that fits.

immutable
Card — a card is a card; String
mutable
Rectangle — shapes get moved and resized; StringBuilder — built up piece by piece
imm
The object represents a fixed value that has no meaningful notion of changing. Making it immutable means it can be shared freely with no aliasing risk.
mut
The object represents something that genuinely changes over time, or that is built up incrementally where creating a new object each time would be wasteful.

53. Make the class immutable

Fill the middle

Prevent any future method from modifying the data.

Fill in the blanks

public class Card final} int rank;
private final int suit;
}

Why: final allows each variable to be initialised once — in the constructor — and rejects any later assignment at compile time. Simply omitting setters achieves the same thing today, but records nothing and stops working the moment someone adds one.

54. Why does immutability matter more in a team?

Real world

Alone, you could just remember not to add a setter.

Discussion prompt

Think Java's phrasing is that a fool might come along later and add a modifier. Why does that argument get stronger as a program grows, and who is the fool usually?

Hint: How long do you remember your own design decisions?

Answer:

The fool is usually you, some months later, with no memory of why the class had no setters. Nothing in the code said it was a decision rather than an omission.

In a team it is worse: the person adding the setter never made the decision at all, and has no way to discover it. A reasonable-looking change silently invalidates assumptions elsewhere.

final turns a decision into a fact the compiler checks. That is why it is worth two keywords — and it is the same argument as private instance variables in Lesson 11a: encode your intentions somewhere the language can defend them.

55. Three kinds of variable, complete

Comparison

Chapter 12 adds the last one. Fill the blanks.

Comparison matrix

localinstanceclass
declaredinside a methodin the class, no staticin the class, with static
how many copiesone per callone per objectone, ever
allocatedon invocationwhen the object is createdwhen the program begins
usable in a static method?yes, its ownnoyes

The bottom row explains an error you have probably hit: main is static, so it cannot use instance variables — but it can use class variables freely, which is why Card.SUITS works anywhere.

56. The pattern to carry away

Pattern

Encoding a set of values as integers, then decoding for display, is a technique you will reuse well beyond cards.

// 1. encode: choose a mapping, and write it down
//    Clubs -> 0, Diamonds -> 1, Hearts -> 2, Spades -> 3
private final int suit;

// 2. decode: an array whose INDEX ORDER is the mapping
public static final String[] SUITS =
    {"Clubs", "Diamonds", "Hearts", "Spades"};

// 3. display: use the value as an index
public String toString() {
    return RANKS[this.rank] + " of " + SUITS[this.suit];
}

// 4. compare: the integers make an order possible
public int compareTo(Card that) { ... }
the encoding buysthe decoding array buys
comparison with < and >readable output
use as an array indexthe mapping written down in code
compact storageone shared copy, if static
a sortable typea place to change the names once

57. Check: class variables

Check

Work it out before you click.

public class Card {
    public static final String[] SUITS = {"Clubs", ...};
    private int suit;
}
// 52 Card objects are created
variablecopies
suit52
SUITS1

Check your understanding

How many copies of each variable exist?

  • A. 52 suits and one SUITS array (correct)
  • B. 52 of each
  • C. one of each
  • D. 52 SUITS arrays and one suit

Answer: A

Why: An instance variable exists once per object, so 52 cards have 52 suit variables. A class variable is declared static and shared across all instances, so there is exactly one SUITS array however many cards exist — and it exists even before the first one is created.

Why B tempts people
This treats SUITS as an instance variable, but static is precisely what makes it shared.
Why C tempts people
Each card genuinely needs its own suit, which is why suit is an instance variable rather than static.
Why D tempts people
This reverses the two — the static one is the shared one.

58. Check: compareTo

Check

Work it out before you click.

// suit is compared first
Card a = new Card(13, 0);   // King of Clubs
Card b = new Card(2, 1);    // 2 of Diamonds
a.compareTo(b)
comparisonvalues
suits0 against 1
resultthis suit is lower

Check your understanding

What does compareTo return?

  • A. -1 — Clubs sorts before Diamonds, and suit is compared first (correct)
  • B. 1 — a King outranks a 2
  • C. 0
  • D. 11 — the rank difference

Answer: A

Why: The method compares suits before ranks, so Clubs (0) against Diamonds (1) settles it and it returns -1 without ever looking at the ranks. The King's higher rank is irrelevant given the decision that suit matters more — a decision the class made and a different game could reverse.

Why B tempts people
Rank is only consulted when the suits are equal, which they are not here.
Why C tempts people
Zero is returned only when both the suit and the rank match.
Why D tempts people
This version of compareTo returns -1, 0 or 1 — it never returns a difference. String's version does, which is why callers should test only the sign.

59. Check: final

Check

Work it out before you click.

public class Card {
    private final int rank;

    public Card(int rank) {
        this.rank = rank;
    }

    public void setRank(int r) {
        this.rank = r;
    }
}
assignmentlegal?
in the constructoryes
in setRankno

Check your understanding

What happens when you compile this?

  • A. A compile error in setRank — a final variable cannot be reassigned (correct)
  • B. It compiles; final only prevents reassigning the whole object
  • C. A compile error in the constructor
  • D. It compiles and setRank silently does nothing

Answer: A

Why: A final instance variable may be initialised once, in the constructor, and never assigned again — so the constructor is fine and the setter is rejected. That is exactly the safeguard the book recommends: it makes adding a modifier impossible rather than merely discouraged.

Why B tempts people
final on an instance variable prevents reassigning that variable; the object-reference case is a separate use of the same keyword.
Why C tempts people
The constructor holds the one permitted assignment — that is how a final field gets its value at all.
Why D tempts people
Java does not silently ignore illegal assignments; it refuses to compile.

60. Encoding is everywhere

Real world

Mapping a set of things onto integers so they can be compared, indexed and stored compactly is one of the oldest techniques in programming.

Discussion prompt

Cards became integers so they could be compared and used as indexes. Where else have you seen a set of non-numeric things represented as numbers — including earlier in this book?

Hint: Lesson 6a and Lesson 7b both used one.

Answer:

Characters are the biggest example: Unicode encodes every letter as a code point, which is what made letter - 'a' work in Lesson 7b and for (char c = 'a'; c <= 'z'; c++) work in Lesson 6a.

Beyond this book: colours as RGB numbers, days of the week as 0 to 6, error codes, database keys, and every file format ever designed. The reason is always the same — numbers compare, index and store in ways that names do not.

And the cost is always the same too: the mapping is a convention the compiler cannot check, so it has to be written down. The SUITS array is that write-down, which is why making it a class variable is worth more than a comment.

61. How sure are you?

Commit first

Commit to an answer and to your confidence.

Predict first

In public static final String[] SUITS, what does final prevent?

  • Reassigning SUITS to a different array — but not changing its elements
  • Changing any element of the array
  • Other classes reading the array
  • Creating more than one copy of the array

Correct: Reassigning SUITS to a different array — but not changing its elements

Why: Think Java words this carefully: final means the variable — or in this case the reference — is constant. So SUITS = new String[4]; is rejected while SUITS[0] = "Wands"; compiles perfectly and changes how every Card in the program decodes. That gap matters because SUITS is also public. The static keyword is what ensures a single shared copy, and public is what allows other classes to read it — three modifiers, three separate effects.

62. Explain it to someone else

Explain it

Two minutes, out loud.

Discussion prompt

A classmate asks why Card stores integers rather than Strings, since the integers have to be decoded for display anyway. Give them the argument, including one concrete comparison that Strings get wrong.

Hint: Compare a 10 and a 9 alphabetically.

Answer:

Strings display well and compare badly. < and > do not work on objects at all, and String's compareTo gives alphabetical order — so "10" sorts before "9", because the character '1' comes before '9'. That is exactly wrong for ranks.

Integers compare correctly with < and >, and they can be used as array indexes — which is how the decoding works. You pay for it by needing a toString, and that is six lines you write once.

The general form worth adding: choose the representation that makes the hard operation easy. Comparison and sorting are the hard part here, and displaying is the easy part, so the encoding wins.

63. Exit ticket

Exit ticket

One question before you close the deck.

Predict first

What is the difference between an instance variable and a class variable?

  • An instance variable has one copy per object; a class variable is declared static and has one copy shared by all of them
  • A class variable is private and an instance variable is public
  • A class variable can be changed and an instance variable cannot
  • They are the same thing with different names

Correct: An instance variable has one copy per object; a class variable is declared static and has one copy shared by all of them

Why: The static keyword is what makes the difference: a class variable is allocated when the program begins and persists until it ends, regardless of how many objects exist — while instance variables are created with each object and garbage-collected with it. That is why a decoding table like SUITS should be static (every card needs the same one) while suit should not (every card needs its own). Visibility and finality are separate decisions from this one.

64. Draw the whole lesson

Connect it up

One page, from memory.

Draw it

Write out both encodings — suits to 0-3 and ranks to 1-13 — and beside them draw the SUITS array with its indexes, showing how the index order IS the mapping. Then write the Card class skeleton with one class variable, two final instance variables and a constructor, labelling how many copies of each exist when 52 cards have been created. Finally, trace compareTo for the King of Clubs against the 2 of Diamonds and say which decision made the answer come out as it did.

65. Recap

Recap

Five sections that design a class carefully — and every decision in it is one Chapters 13 and 14 will rely on.

if you remember one thingit is this
about encodingchoose what makes the hard operation easy
about staticone copy for the class, not one per object
about immutabilityfinal records the decision and defends it

Sources

  1. Downey & Mayfield, Think Java, 2nd edition (Green Tea Press / O'Reilly, 2020) — Think Java 2e, Chapter 12 (Arrays of Objects), Sections 12.1-12.5, pp. 201-208
  2. Java SE 21 API — java.lang.Comparable
  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