LLD Dojo

Java Bridge · J10

Enums as real classes — constants that carry state and behaviour

The idea

A constant is an object, so it can carry its own value

The vending machine takes four coins. Read enum Coin { NICKEL, DIME, QUARTER, DOLLAR } as four named integers and the value has to live somewhere else, so you write the table:

private static final Map<Coin, Integer> MINOR_VALUE = Map.of(
        Coin.NICKEL, 5, Coin.DIME, 10, Coin.QUARTER, 25);

That was right the day it was written, when the machine took three denominations. It takes four now. Nothing stopped compiling when DOLLAR was declared, because a map has no opinion about which keys it ought to hold. You find out at the till:

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the
return value of "java.util.Map.get(Object)" is null

So put the number where it cannot drift. A Java enum is a class with a fixed set of instances, and an instance can hold a final field:

NICKEL(5), DIME(10), QUARTER(25), DOLLAR(100);

private final int minorValue;

Coin(int minorValue) { this.minorValue = minorValue; }

QUARTER.minorValue() returns 25 because QUARTER is an object that was constructed with 25. The constructor runs once per constant, and it is private whether you write a modifier or not. Those four are the only Coin instances that will ever exist.

The stale table is now unwritable. A fifth denomination cannot be declared without a value, because the constructor demands one, and javac asks at the declaration rather than letting a map answer null in production.

Constants can carry methods too, one body each. That is the part with no C++ equivalent, and worked/NOTES.md gives the threshold for reaching for it.


Coming from C++

From C++ — the same four coins, and why one of them can answer questions

enum class was the biggest thing C++11 fixed about enums, so it is reasonable to arrive thinking Java's version is the same idea with a different keyword. It is not the same kind of construct at all, and this page is the delta.

The declaration

C++ — the constant is the number

enum class Coin : int {
    NICKEL  = 5,
    DIME    = 10,
    QUARTER = 25,
    DOLLAR  = 100,
};

int minorValue(Coin c) { return static_cast<int>(c); }

The value is the enumerator. One number per constant, and no room for a second. The moment a coin needs two facts about itself you reach for a switch in a free function, or a constexpr lookup table indexed by the cast.

Java — the constant is an object that was handed a number

public enum Coin {

    NICKEL(5),
    DIME(10),
    QUARTER(25),
    DOLLAR(100);

    private final int minorValue;

    Coin(int minorValue) {
        this.minorValue = minorValue;
    }

    public int minorValue() {
        return minorValue;
    }
}

Read the four lines at the top as four constructor calls, because that is what they are. Coin.QUARTER is a reference to an object built by new Coin(25), running at class initialisation, before any of your code touches it. The reflection in worked/Main.java block 10 says so out loud:

  Coin extends              : java.lang.Enum
  QUARTER.getClass()        : Coin

There is no cast from Coin to int and no underlying integral type to declare. Try it:

Widen.java:2: error: incompatible types: Coin cannot be converted to int
    static int asInt(Coin coin) { return coin; }
                                         ^

Room for a second fact costs nothing now. Slot in the same corpus problem carries four; a coin that also needed a display glyph and a weight in grams would take two more constructor parameters and no new file.

The delta table

C++ enum classJava enum
A distinct integral type with named valuesA final class extending java.lang.Enum, with a fixed instance set
static_cast<int>(Coin::QUARTER) is 25Coin.QUARTER.minorValue() is 25; ordinal() is 2
Underlying type declarable: : int, : uint8_tNo underlying type. A constant is a reference
Cannot hold data or methodsFields, methods, constructors, interfaces
No mapping from a name to a value at run timename(), valueOf(String), values(), toString()
Coin::QUARTER in a case labelcase QUARTER bare, or case Coin.QUARTER since Java 21
switch on an enum warns under -WswitchOnly a switch expression errors; a statement is silent
Free function + switch for per-constant behaviourA method body on the constant itself
int counts[COIN_COUNT] with a sentinel constantEnumMap<Coin, Integer>, and never a sentinel
Can be forward declared with its underlying typeNo forward declaration exists, and none is needed

Trap 1 · ordinal() is not the value, and it moves

The muscle memory to break. In C++ the integer behind the constant is the thing you chose. In Java it is the position in the declaration list, counting from zero, and you chose nothing.

  NICKEL ordinal=0
  DIME ordinal=1
  QUARTER ordinal=2
  DOLLAR ordinal=3

The corpus has a curveball that puts the fifty-cent piece back into circulation (corpus/vending-machine/curveballs/01-a-new-coin/). Its revised Coin inserts HALF_DOLLAR(50) between the quarter and the dollar. Run the same loop against that file:

  NICKEL ordinal=0
  DIME ordinal=1
  QUARTER ordinal=2
  HALF_DOLLAR ordinal=3
  DOLLAR ordinal=4
  ordinal 3 reads back as : HALF_DOLLAR
  name "DOLLAR" reads back as: DOLLAR
  and it is still worth      : 100

A row written yesterday holding 3 for a dollar reads back as a half-dollar today. Nothing threw, nothing logged, and the audit is now wrong by fifty cents per row.

So: never persist ordinal(), and never send it over a wire. Persist name(), which is the identifier you typed and changes only when you rename the constant. The rule holds for a database column, a JSON field, a cache key and a file format. ordinal() earns its keep inside the process that produced it, which is exactly what EnumMap and EnumSet do with it.

Trap 2 · The sentinel constant that ends every C++ enum

COIN_COUNT or MAX as a last enumerator is ordinary C++, because you need a size for the array. In Java it is a coin the machine accepts:

  values()      : [NICKEL, DIME, QUARTER, DOLLAR, COUNT]
  largest first : [DOLLAR, QUARTER, DIME, NICKEL, COUNT]
  COUNT is a coin the machine now accepts: 0

values(), every for loop over it, every exhaustive switch and every EnumSet.allOf now include it. The size you wanted is Coin.values().length, and new EnumMap<>(Coin.class) sizes itself, so the sentinel has no job left to do.

Trap 3 · == is the right comparison here, and only here

J6 spent a page on == comparing references and never values. Enums are the exception, and the reason is mechanical rather than a special case in the language:

  valueOf("QUARTER") == QUARTER : true
  distinct instances            : 4

The constructor is private, the class cannot be extended, and valueOf looks up the existing instance rather than building one. So two references to the same constant are always the same object, and == is both correct and the convention. equals is final on java.lang.Enum and delegates to == anyway.

Two consequences worth keeping. == on enums gives you a compile error on a type mismatch, where equals would silently return false. And a switch over an enum is legal precisely because the constant set is closed.

Trap 4 · valueOf throws where you expect a null or a sentinel

C++ has no name-to-constant lookup, so there is no habit to unlearn — there is only a new API to read correctly.

  java.lang.IllegalArgumentException: No enum constant Coin.HALF_DOLLAR
  java.lang.IllegalArgumentException: No enum constant Coin.quarter

Both lines are real output from worked/Main.java. Note the second: the match is exact and case sensitive, so a lowercased value from a config file or a JSON body throws. IllegalArgumentException is unchecked, so nothing forces you to handle it and nothing warns you that you have not. Parsing untrusted text into an enum is the one place to write the try/catch, and the one place a values() scan with your own fallback is worth the lines.

Trap 5 · The exhaustiveness habit inverts

In C++ you get -Wswitch on a plain switch statement over an enum class, and under -Werror that is your safety net. Java moves the check: it is an error, not a warning, but only for a switch expression — the form that produces a value.

The corpus has the real one. corpus/parking-lot/reference/src/Footprint.java line 30:

    public static Footprint of(VehicleType type) {
        return switch (type) {
            case MOTORBIKE -> MOTORBIKE;
            case CAR -> CAR;
            case TRUCK -> TRUCK;
        };
    }

Add a fourth vehicle type to VehicleType and that file stops compiling:

Footprint.java:30: error: the switch expression does not cover all possible input values
        return switch (type) {
               ^
1 error

That is the mechanism behind "add a constant, let the compiler find every site", and it is what makes an interviewer's new vehicle type cheap to absorb. Two details to get right:

The message does not name the constant you missed. It names the file, the line and the selector, and stops there. Read it as "count your cases against values()".

A switch statement doing the same job is silent. The same three cases, written as a statement with a mutable local, compiled under -Xlint:all with no warning at all and then:

  MOTORBIKE occupies 1 spot(s)
  CAR occupies 1 spot(s)
  TRUCK occupies 2 spot(s)
  VAN occupies 1 spot(s)

The van silently takes one spot. So prefer the expression form whenever the switch produces a value, and know that a default branch buys the same silence back by making every future constant covered.

Trap 6 · No inheritance, so a hierarchy has to go elsewhere

Extend.java:2: error: cannot inherit from final Coin
Extend.java:2: error: enum classes are not extensible

An enum with no constant bodies compiles to a final class. Give any constant a body and the class becomes abstract instead, with one anonymous subclass per body:

  Coin is final             : true
  MachineState is abstract  : true
  IDLE.getClass()           : MachineState$1
  IDLE.getDeclaringClass()  : MachineState

Those subclasses are real files on disk — MachineState$1.class, $2, $3. What you cannot do is extend an enum yourself, from anywhere. When you want an open set, the tool is an interface with classes behind it, which is J5 and B1 territory. The enum is for a set that is closed and expected to stay closed. Coin is exactly that, and the corpus file says so: a new denomination arrives as a new constant here.


Worked walkthrough

NOTES — six files, and the two shapes an enum can take

Run it first

.toolchain\jdk-21\bin\javac.exe -d out *.java
.toolchain\jdk-21\bin\java.exe -cp out Main

Real output, verbatim:

--- 1. a constant carries its own value
  NICKEL -> 5
  DIME -> 10
  QUARTER -> 25
  DOLLAR -> 100
  largest first  : [DOLLAR, QUARTER, DIME, NICKEL]
--- 2. one constant, one instance, forever
  valueOf("QUARTER") == QUARTER : true
  QUARTER.equals(QUARTER)       : true
  distinct instances            : 4
--- 3. the side table, one denomination out of date
  three known coins total : 35
  java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
--- 4. the field cannot be out of date
  four denominations, one line : 135
  count(NICKEL) with none held : 0
  has an entry per denomination: false
--- 5. iteration order
  inserted        : [QUARTER, NICKEL, DIME]
  EnumMap  order  : [NICKEL, DIME, QUARTER]
  HashMap  order  : [QUARTER, NICKEL, DIME]   (unspecified)
--- 6. values() is a fresh array every call
  values() == values() : false
  after overwriting it : [NICKEL, DIME, QUARTER, DOLLAR]
--- 7. ordinal() versus name()
  NICKEL ordinal=0
  DIME ordinal=1
  QUARTER ordinal=2
  DOLLAR ordinal=3
  DOLLAR.ordinal() persisted today : 3
  read back as                     : DOLLAR
--- 8. valueOf on a name that is not there
  java.lang.IllegalArgumentException: No enum constant Coin.HALF_DOLLAR
  java.lang.IllegalArgumentException: No enum constant Coin.quarter
--- 9. each state answers for itself
  state            INSERT_COIN  SELECT   COLLECT   REFUND   busy
  IDLE             true         true     false     true     false
  ACCEPTING_MONEY  true         true     false     true     false
  DISPENSING       false        false    true      false    true
--- 10. what a constant actually is
  Coin extends              : java.lang.Enum
  QUARTER.getClass()        : Coin
  Coin is final             : true
  MachineState is abstract  : true
  IDLE.getClass()           : MachineState$1
  IDLE.getDeclaringClass()  : MachineState

Everything above reproduces except one line. The HashMap order line in block 5 is not a guarantee. It came out in insertion order on this run, which proves nothing: HashMap places enum keys by their identity hash, and no order is documented. An order you cannot rely on is worse than a wrong one, because your test passes.

Note out/ afterwards:

Coin.class            MachineEvent.class      MachineState$2.class   Main.class
CoinPurse.class       MachineState.class      MachineState$3.class
LooseCoinPurse.class  MachineState$1.class

Nine class files from six source files. MachineState$1, $2 and $3 are the three constants with bodies, compiled as three anonymous subclasses. That is the strongest single answer to "an enum is a named integer": three of them are on your disk with their own bytecode.


Coin.java — the shape to memorise

    NICKEL(5),
    DIME(10),
    QUARTER(25),
    DOLLAR(100);

Four constructor calls, run once, at class initialisation. Each one produces an object, and that object is what Coin.NICKEL refers to for the life of the JVM. The invariant the JVM holds for you: class initialisation happens once, under a lock, before any thread can observe a constant. So an enum constant is a lazily initialised singleton with no double-checked locking and no volatile field anywhere. That is why enum is the recommended way to write a singleton in Java.

The semicolon after the last constant is what lets members follow. Without a member section the comma-separated list can end bare, and that is why MachineEvent.java is one line.

    private final int minorValue;

final, so the value is fixed before anything can read it, and private, so nothing can reach around the accessor. Drop final, add a setter, and you have built a mutable singleton. One object shared by the whole process, and any caller can change what a quarter is worth for everyone. That failure has no local reproduction, because the corruption happens in whichever code touched it first.

    Coin(int minorValue) {
        this.minorValue = minorValue;
    }

No access modifier, and adding public is a compile error:

PublicCtor.java:4: error: modifier public not allowed here
    public PublicCtor(int minorValue) { this.minorValue = minorValue; }
           ^
1 error

An enum constructor is private by construction. That is not a style rule the compiler is enforcing; it is the mechanism that makes the four constants the complete set. new Coin(7) is refused for the same reason:

Bad.java:13: error: enum classes may not be instantiated
        return new Coin(7);
               ^

What this constructor guarantees that a lookup table cannot. A fifth constant cannot be declared without an argument, because there is no no-argument constructor to bind to. So "a coin exists whose value nobody wrote down" is not a state the program can reach. Compare with MINOR_VALUE in LooseCoinPurse.java, where exactly that state is one edit away and compiles silently.

    public int minorValue() {
        return minorValue;
    }

A method, not a public field, so the coin can start computing the answer later without any caller changing. Java has no properties and no way to make a field into a method compatibly, so the accessor is the cheap decision that keeps that door open. Naming it minorValue() rather than getMinorValue() is the corpus convention and matches what record generates.


LooseCoinPurse.java — the version that treats a constant as a name

Not a straw man. The table is typed, immutable, and it was correct on the day it was written:

    private static final Map<Coin, Integer> MINOR_VALUE = Map.of(
            Coin.NICKEL, 5,
            Coin.DIME, 10,
            Coin.QUARTER, 25);

Nothing here failed to compile when DOLLAR was declared, because a map has no opinion about which keys it ought to contain. The failure surfaces at the till, in totalMinor:

java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null

Read the message: it names the unboxing, the call that returned null and nothing about coins. The line that produced it is (long) MINOR_VALUE.get(held.getKey()) * held.getValue(), and the cast to long is the unboxing site.

Switching this to EnumMap does not fix it, and believing it does is the trap under the trap. An EnumMap missing an entry returns null too — block 11 of the probe run says so:

  EnumMap get(DOLLAR): null

What fixes it is moving the number onto the constant. EnumMap is the right container for counts, which legitimately vary per coin and legitimately start absent. It is the wrong container for facts that must exist for every constant.

    private final Map<Coin, Integer> counts = new HashMap<>();

A HashMap keyed on an enum works and costs more than it needs to. Every get and put hashes the constant's identity, and every count is boxed twice, once as the key's hash input and once as the Integer value. It also gives up iteration order, which is block 5.


CoinPurse.java — the reference's container

    private final Map<Coin, Integer> counts = new EnumMap<>(Coin.class);

The Coin.class argument is what makes this an array rather than a hash table. EnumMap holds a values() snapshot and one Object[] indexed by ordinal(), so a lookup is a bounds check and an array read. No hashing, no key boxing, no collision handling, no resize.

Three guarantees that come with it, in the order they matter:

  1. Iteration is in declaration order, documented, not incidental. Block 5 inserts [QUARTER, NICKEL, DIME] and iterates [NICKEL, DIME, QUARTER]. Every log line, every toString, every test assertion over the key set is now reproducible.
  2. A null key throws immediately, with Cannot invoke "Object.getClass()" because "key" is null. A HashMap accepts null as a key and hides the mistake until something reads it back.
  3. The key space is known, so counts.size() == Coin.values().length is a real completeness check. mentionsEveryCoin() is that line, and it returns false in block 4 because three of four denominations are held.

The speed is genuinely better and it is the least important reason. The reason to reach for EnumMap is that the ordering and the null behaviour are decided rather than left open.

    public int count(Coin coin) {
        return counts.getOrDefault(coin, 0);
    }

getOrDefault is what makes a partial map safe to expose. Absence means zero, which is true of a coin the purse has never seen, so no caller has to handle null and no caller can NPE. That single decision is why MachineFactory.standardFloat() in the corpus can be Map.of(NICKEL, 1, DIME, 2, QUARTER, 1) and survive a new denomination with no edit. The float is a set of exceptions to zero, not a list to maintain.

        total += (long) held.getKey().minorValue() * held.getValue();

getKey().minorValue() is the whole lesson in one expression. The purse asks the coin. It does not know a nickel from a dollar and cannot be made stale by a new denomination.

The cast is on the left operand, before the multiply. (long)(a * b) would multiply two ints, overflow, and then widen the wrong answer. Not reachable with a realistic coin count, and the habit costs nothing.

    public List<Coin> denominationsHeld() {
        return List.copyOf(counts.keySet());
    }

List.copyOf and not the key set itself. A keySet() is a live view: hand it out and a caller can call remove on it and silently empty the purse. The copy also freezes the order, which is what makes this method testable.


MachineState.java — abstract method per constant

This is the feature with no C++ analogue, and the one that gets over-applied. The syntax first:

    IDLE {
        @Override
        public boolean allows(MachineEvent event) {
            return event != MachineEvent.COLLECT;
        }
    },

The braces after the constant name are a class body. IDLE is an instance of an anonymous subclass of MachineState, which is why block 10 prints MachineState$1 for IDLE.getClass() and MachineState for IDLE.getDeclaringClass(). Reach for getClass() on an enum with bodies and you will get the subclass, which breaks any comparison written against the enum class itself.

    public abstract boolean allows(MachineEvent event);

abstract is the entire safety mechanism. It moves "did you decide what this state does?" from a review comment to a compile error. Add the maintenance state from corpus/vending-machine/curveballs/03-maintenance-mode/ and leave it bare:

MachineState.java:24: error: MachineState is abstract; cannot be instantiated
    MAINTENANCE;
    ^
1 error

Read past the wording. The message sounds like it is about new, and it is: a constant declaration is an instantiation, and the class is abstract until every constant supplies the missing body. The caret is on the constant that forgot. Give the method a default implementation in the enum body instead of making it abstract, and this error disappears along with the guarantee.

    public final boolean isBusy() {
        return this == DISPENSING;
    }

final, because the answer does not vary, and a constant that overrode it would be lying. Also note this == DISPENSING rather than equals: == is correct on enums, and it is the convention.

The threshold, because constant bodies are over-applied

Put behaviour in a constant body when all three hold:

  1. Intrinsic. The behaviour is a property of the constant, not a decision about it. What a coin is worth is intrinsic; what you charge for a chocolate bar is a decision.
  2. Closed. There is exactly one right answer per constant, and no caller will ever want a different one. If a second answer is imaginable, that second answer is a second implementation of something, and constant bodies give you nowhere to put it.
  3. Small. A few lines, no collaborators, no state beyond the constant's own fields. A constant body cannot be injected, stubbed, or constructed with a test double, so anything you would want to test in isolation does not belong there.

Reach for a policy interface when any one of those fails. The tell is that the behaviour is a decision that might be swapped, or that you want to test it without the enum.

What the corpus reference actually chose, in both places.

Coin.minorValue is a field with an accessor, not a constant body. A value is data; a body would be a method returning a literal, which is the same fact with more syntax.

The transition rules are not constant bodies. corpus/vending-machine/reference/src/ puts them in Transitions.standard() as a List<Transition> of (from, on, to) records, indexed by TransitionTable into EnumMap<MachineState, EnumMap<MachineEvent, MachineState>>. The version in this lesson is the alternative, and the reference's reasoning is in TransitionTable's own comment. Read it against the three tests above:

TestThe allows body versionThe reference's table
IntrinsicFails. A machine's legal moves are a rule someone wrote, and rules change per deployment
ClosedFails. MaintenanceTransitions.all() is a second rule set for the same states
SmallPasses, barely. Two of the three bodies are byte-identical, which is the tell

That duplication is the signal worth learning to see. When two constant bodies are the same, the thing varying is not the constant. IDLE and ACCEPTING_MONEY answer identically because the rule is about the pair (state, event), and a per-constant method can only be about one of them.

The measured cost, from curveballs/03-maintenance-mode/reference-patch/PATCH.md: the reference absorbs the maintenance mode in 8 changed lines. Six are declarations every design needs, being two enum words and two interface methods. The other two are the rule itself, in a new file that is free under the diff budget.

The constant-body version needs a body for MAINTENANCE, plus an edit to IDLE's body to let the machine leave service. And it cannot express a second rule set at all without a flag inside every body.

One honest point the other way: the constant-body version is refused at compile time if you forget the new state's rules, and the table is not. The reference accepts that, because a state no transition mentions can never be entered, so an incomplete table is unreachable rather than wrong.


Four experiments, run for real

1 · The exhaustive switch, and the half of it that is silent

corpus/parking-lot/reference/src/Footprint.java maps a vehicle type to how many spots it takes, using a switch expression with no default. Its comment claims a new VehicleType stops the file compiling. Adding VAN to VehicleType:

Footprint.java:30: error: the switch expression does not cover all possible input values
        return switch (type) {
               ^
1 error

Claim verified. Two things the message does not do: it does not name VAN, and it does not point at the case labels. It gives you the file, the line and the selector expression.

Then the same three cases as a switch statement with a mutable local, compiled with -Xlint:all:

compile exit 0
  MOTORBIKE occupies 1 spot(s)
  CAR occupies 1 spot(s)
  TRUCK occupies 2 spot(s)
  VAN occupies 1 spot(s)

Zero errors, zero warnings, and a van that fits in a car space. Exhaustiveness is required of switch expressions only. A default branch buys the same silence back, because every future constant is already covered by it. That is the argument for leaving default off when the constant set is closed and you want the compiler to tell you.

2 · values() hands out a copy, every call

  values() == values() : false
  after overwriting it : [NICKEL, DIME, QUARTER, DOLLAR]

The array is cloned on each call, so a caller who mutates it corrupts their own copy and nothing else. The invariant is worth the cost: there is no way to damage the constant set from outside.

The cost is an allocation per call, which matters in one place. for (Coin c : Coin.values()) inside a hot loop allocates an array per iteration of the outer loop. When that shows up in a profile, hold one private static final Coin[] VALUES = values(); and iterate that. EnumSet.allOf and EnumMap already do this internally, which is one more reason to reach for them.

3 · A persisted ordinal() after the coin curveball

curveballs/01-a-new-coin/contract-delta/Coin.java inserts HALF_DOLLAR(50) between the quarter and the dollar. Compiling and running the same loop against it:

  NICKEL ordinal=0
  DIME ordinal=1
  QUARTER ordinal=2
  HALF_DOLLAR ordinal=3
  DOLLAR ordinal=4
  ordinal 3 reads back as : HALF_DOLLAR
  name "DOLLAR" reads back as: DOLLAR
  and it is still worth      : 100

A row stored yesterday holding 3 for a dollar reads back as a half-dollar today. No exception, no log line, and every reconciliation from before the deploy is now wrong by fifty cents.

name() survived the same edit because it is the identifier you typed. So: ordinal() is for EnumMap and EnumSet inside one process, name() is for anything that outlives it.

4 · The sentinel constant, imported from C++

COIN_COUNT as a final enumerator is ordinary C++ and a bug here:

  values()      : [NICKEL, DIME, QUARTER, DOLLAR, COUNT]
  largest first : [DOLLAR, QUARTER, DIME, NICKEL, COUNT]
  COUNT is a coin the machine now accepts: 0

GreedyChangeMaker would try to pay change with it, EnumSet.allOf would include it, and every exhaustive switch would demand a case for it. The number you wanted is Coin.values().length.


What an interviewer is measuring

The reason this item is in the bridge track rather than the syllabus is that it is Java mechanics. The reason it has scoring consequences is the curveball. A new coin denomination costs the reference zero lines, measured in curveballs/01-a-new-coin/reference-patch/PATCH.md. It costs nothing because the value lives on the constant, the change-maker derives its denominations from values(), and the purse is an EnumMap. Every plausible alternative is an edit per site: a switch (coin) for the value, an int[] of denominations, a float declared with an entry per coin.

Say the reason out loud when you write it. "The value lives on the constant so a new denomination cannot be declared without one" is a design sentence. "I used an enum" is not.


Worked source

The 6 files of the worked design

Every file below is the one the app opens, verbatim. This is the part worth reading slowly: the prose above argues for a shape, and these are the lines that have it.

worked/src/Coin.java33 lines

// Coin.java — this is corpus/vending-machine/contract/Coin.java, unchanged.
//
// The shape to memorise: a value in the constant declaration, a final field, an accessor.
// Four constants, four objects, and each one knows what it is worth.

/**
 * The coins this machine accepts, and what each is worth in minor units.
 *
 * The enum <i>is</i> the accepted set: there is no such thing as a coin the machine does
 * not recognise, so {@code insertCoin} has no "bad coin" failure mode. If a new
 * denomination ever comes into circulation it arrives as a new constant here.
 */
public enum Coin {

    NICKEL(5),
    DIME(10),
    QUARTER(25),
    DOLLAR(100);

    private final int minorValue;

    // No modifier is legal here and none is needed. An enum constructor is private whether
    // you say so or not, which is what makes the four constants above the only four instances
    // that will ever exist.
    Coin(int minorValue) {
        this.minorValue = minorValue;
    }

    /** What the coin is worth, in minor units (cents). Always positive. */
    public int minorValue() {
        return minorValue;
    }
}

worked/src/CoinPurse.java54 lines

// CoinPurse.java — trimmed from corpus/vending-machine/reference/src/CoinPurse.java.
//
// The reference holds coins in an EnumMap and asks each Coin what it is worth. Nothing in
// this file names a denomination, so a new one is absorbed with no edit.
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/** A multiset of coins that can total itself. The escrow and the hopper are both one of these. */
public final class CoinPurse {

    private final Map<Coin, Integer> counts = new EnumMap<>(Coin.class);

    public void add(Coin coin) {
        counts.merge(Objects.requireNonNull(coin, "coin"), 1, Integer::sum);
    }

    /** Zero for a denomination the purse has never seen, rather than null. */
    public int count(Coin coin) {
        return counts.getOrDefault(coin, 0);
    }

    /**
     * What the purse is worth, asking each coin rather than looking the value up anywhere.
     *
     * The cast to long is on the multiplication, not after it: 25 * 100_000_000 overflows an
     * int and a vending machine that has taken a hundred million quarters would report a
     * negative float.
     */
    public long totalMinor() {
        long total = 0;
        for (Map.Entry<Coin, Integer> held : counts.entrySet()) {
            total += (long) held.getKey().minorValue() * held.getValue();
        }
        return total;
    }

    /** The denominations actually held, in declaration order — an EnumMap guarantees that. */
    public List<Coin> denominationsHeld() {
        return List.copyOf(counts.keySet());
    }

    /**
     * Whether the purse has an entry for every denomination that exists.
     *
     * Only askable because the key space is a closed set the program can enumerate. It is a
     * run-time check, and the compile-time version of the same idea is Coin's final field:
     * a constant cannot be declared without a value.
     */
    public boolean mentionsEveryCoin() {
        return counts.size() == Coin.values().length;
    }
}

worked/src/LooseCoinPurse.java42 lines

// LooseCoinPurse.java — the version written when Coin was a named integer.
//
// "Loose" because the value lives loose in a side table instead of on the constant. This is
// what a C++ developer writes on day one, and it is not a straw man: the table is typed, it
// is immutable, and it was correct on the day it was written.
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class LooseCoinPurse {

    /**
     * What each coin is worth. Written the day the machine took three denominations.
     *
     * The machine now takes four. Nothing here or anywhere else failed to compile when DOLLAR
     * was declared, because a map has no opinion about which keys it ought to contain.
     */
    private static final Map<Coin, Integer> MINOR_VALUE = Map.of(
            Coin.NICKEL, 5,
            Coin.DIME, 10,
            Coin.QUARTER, 25);

    private final Map<Coin, Integer> counts = new HashMap<>();

    public void add(Coin coin) {
        counts.merge(coin, 1, Integer::sum);
    }

    /** Throws NullPointerException on any coin the table forgot. See Main, block 3. */
    public long totalMinor() {
        long total = 0;
        for (Map.Entry<Coin, Integer> held : counts.entrySet()) {
            total += (long) MINOR_VALUE.get(held.getKey()) * held.getValue();
        }
        return total;
    }

    /** Unspecified order. A HashMap keyed on an enum hashes the constant's identity. */
    public List<Coin> denominationsHeld() {
        return List.copyOf(counts.keySet());
    }
}

worked/src/MachineEvent.java10 lines

// MachineEvent.java — corpus/vending-machine/reference/src/MachineEvent.java, unchanged.

/**
 * The things that can happen TO the machine — the alphabet of the state machine.
 *
 * <p>An enum with no fields and no methods is still a class with four instances. It is the
 * one case where the C++ reading costs you nothing, and it is here so the next file has
 * something to be exhaustive over.
 */
public enum MachineEvent { INSERT_COIN, SELECT, COLLECT, REFUND }

worked/src/MachineState.java55 lines

// MachineState.java — NOT the corpus file. Read the warning below before copying this shape.
//
// The corpus contract declares this enum with no bodies at all:
//
//     public enum MachineState { IDLE, ACCEPTING_MONEY, DISPENSING }
//
// and keeps the transition rules in Transitions.standard() as a list of Transition records,
// indexed by TransitionTable. This file is the other design — the rules as behaviour on each
// constant — written out so you can see the syntax and then judge the trade. NOTES.md gives
// the threshold and says which one the reference picked, and why.

/**
 * Where the machine is in a transaction, with each state answering for itself which
 * operations it will accept.
 *
 * <p>The rows are the contract's transition table: IDLE and ACCEPTING_MONEY take a coin, a
 * selection or a refund; DISPENSING has one way out and it is {@code COLLECT}.
 */
public enum MachineState {

    IDLE {
        @Override
        public boolean allows(MachineEvent event) {
            return event != MachineEvent.COLLECT;
        }
    },

    ACCEPTING_MONEY {
        @Override
        public boolean allows(MachineEvent event) {
            return event != MachineEvent.COLLECT;
        }
    },

    DISPENSING {
        @Override
        public boolean allows(MachineEvent event) {
            return event == MachineEvent.COLLECT;
        }
    };

    /**
     * Whether this state will accept this operation.
     *
     * Abstract, so the enum will not compile until every constant has answered. A new state
     * added below with no body is a compile error naming the state, which is the whole reason
     * to write it this way rather than as a switch with a default.
     */
    public abstract boolean allows(MachineEvent event);

    /** Shared by every constant, because the answer does not vary. Not overridable. */
    public final boolean isBusy() {
        return this == DISPENSING;
    }
}

worked/src/Main.java146 lines

// Main.java — every claim in NOTES.md, run.
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        constantsCarryTheirValue();
        eachConstantIsOneObject();
        theSideTableThatWentStale();
        theFieldThatCannot();
        mapOrder();
        valuesIsACopy();
        ordinalIsPositional();
        valueOfIsStrict();
        statesAnswerForThemselves();
        whatTheCompilerProduced();
    }

    /** 1 — four constants, four objects, each holding a number nothing else has to know. */
    private static void constantsCarryTheirValue() {
        System.out.println("--- 1. a constant carries its own value");
        for (Coin coin : Coin.values()) {
            System.out.println("  " + coin + " -> " + coin.minorValue());
        }
        // The change-maker's real line: the denominations AND their order are derived.
        List<Coin> largestFirst = Arrays.stream(Coin.values())
                .sorted(Comparator.comparingInt(Coin::minorValue).reversed())
                .toList();
        System.out.println("  largest first  : " + largestFirst);
    }

    /** 2 — the one place in Java where == on objects is the right comparison. */
    private static void eachConstantIsOneObject() {
        System.out.println("--- 2. one constant, one instance, forever");
        System.out.println("  valueOf(\"QUARTER\") == QUARTER : " + (Coin.valueOf("QUARTER") == Coin.QUARTER));
        System.out.println("  QUARTER.equals(QUARTER)       : " + Coin.QUARTER.equals(Coin.QUARTER));
        System.out.println("  distinct instances            : "
                + Arrays.stream(Coin.values()).distinct().count());
    }

    /** 3 — the failure the named-integer model leads to, with the real exception. */
    private static void theSideTableThatWentStale() {
        System.out.println("--- 3. the side table, one denomination out of date");
        LooseCoinPurse loose = new LooseCoinPurse();
        loose.add(Coin.QUARTER);
        loose.add(Coin.DIME);
        System.out.println("  three known coins total : " + loose.totalMinor());

        loose.add(Coin.DOLLAR);
        try {
            System.out.println("  unreachable: " + loose.totalMinor());
        } catch (NullPointerException e) {
            System.out.println("  " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    /** 4 — the same arithmetic, asking the constant instead of a table. */
    private static void theFieldThatCannot() {
        System.out.println("--- 4. the field cannot be out of date");
        CoinPurse purse = new CoinPurse();
        purse.add(Coin.QUARTER);
        purse.add(Coin.DIME);
        purse.add(Coin.DOLLAR);
        System.out.println("  four denominations, one line : " + purse.totalMinor());
        System.out.println("  count(NICKEL) with none held : " + purse.count(Coin.NICKEL));
        System.out.println("  has an entry per denomination: " + purse.mentionsEveryCoin());
    }

    /** 5 — EnumMap iterates in declaration order. HashMap iterates in no stated order. */
    private static void mapOrder() {
        System.out.println("--- 5. iteration order");
        CoinPurse purse = new CoinPurse();
        LooseCoinPurse loose = new LooseCoinPurse();
        for (Coin coin : List.of(Coin.QUARTER, Coin.NICKEL, Coin.DIME)) {
            purse.add(coin);
            loose.add(coin);
        }
        System.out.println("  inserted        : [QUARTER, NICKEL, DIME]");
        System.out.println("  EnumMap  order  : " + purse.denominationsHeld());
        System.out.println("  HashMap  order  : " + loose.denominationsHeld() + "   (unspecified)");
    }

    /** 6 — values() hands out a fresh array, so a caller cannot corrupt the constant set. */
    private static void valuesIsACopy() {
        System.out.println("--- 6. values() is a fresh array every call");
        System.out.println("  values() == values() : " + (Coin.values() == Coin.values()));
        Coin[] mine = Coin.values();
        mine[0] = Coin.DOLLAR;
        System.out.println("  after overwriting it : " + Arrays.toString(Coin.values()));
    }

    /** 7 — ordinal() is a position in a list of declarations, and declarations move. */
    private static void ordinalIsPositional() {
        System.out.println("--- 7. ordinal() versus name()");
        for (Coin coin : Coin.values()) {
            System.out.println("  " + coin.name() + " ordinal=" + coin.ordinal());
        }
        System.out.println("  DOLLAR.ordinal() persisted today : " + Coin.DOLLAR.ordinal());
        System.out.println("  read back as                     : " + Coin.values()[Coin.DOLLAR.ordinal()]);
    }

    /** 8 — valueOf throws rather than returning null, and it is case sensitive. */
    private static void valueOfIsStrict() {
        System.out.println("--- 8. valueOf on a name that is not there");
        for (String name : List.of("HALF_DOLLAR", "quarter")) {
            try {
                System.out.println("  unreachable: " + Coin.valueOf(name));
            } catch (IllegalArgumentException e) {
                System.out.println("  " + e);
            }
        }
    }

    /** 9 — the contract's transition table, answered by the states themselves. */
    private static void statesAnswerForThemselves() {
        System.out.println("--- 9. each state answers for itself");
        System.out.printf("  %-16s %-12s %-8s %-9s %-8s %s%n",
                "state", "INSERT_COIN", "SELECT", "COLLECT", "REFUND", "busy");
        for (MachineState state : MachineState.values()) {
            System.out.printf("  %-16s %-12s %-8s %-9s %-8s %s%n",
                    state,
                    state.allows(MachineEvent.INSERT_COIN),
                    state.allows(MachineEvent.SELECT),
                    state.allows(MachineEvent.COLLECT),
                    state.allows(MachineEvent.REFUND),
                    state.isBusy());
        }
    }

    /** 10 — what a constant actually is, according to the class files on disk. */
    private static void whatTheCompilerProduced() {
        System.out.println("--- 10. what a constant actually is");
        System.out.println("  Coin extends              : " + Coin.class.getSuperclass().getName());
        System.out.println("  QUARTER.getClass()        : " + Coin.QUARTER.getClass().getName());
        System.out.println("  Coin is final             : "
                + Modifier.isFinal(Coin.class.getModifiers()));
        System.out.println("  MachineState is abstract  : "
                + Modifier.isAbstract(MachineState.class.getModifiers()));
        System.out.println("  IDLE.getClass()           : " + MachineState.IDLE.getClass().getName());
        System.out.println("  IDLE.getDeclaringClass()  : "
                + MachineState.IDLE.getDeclaringClass().getName());
    }
}

The faded stage is not here, on purpose

In the app, the third stage of a lesson hands you the worked design with a few lines replaced by // GAP: markers, then compiles your completion and runs a JUnit suite against it. That needs javac, and a static site has no compiler — so rather than show a control that cannot work, this page stops at the worked source.

Run the app for the drill: it is the download in the header, and it works offline once unpacked.

← J9 · Exceptions and cleanup — checked vs unchecked, and try-with-resources instead of RAII J11 · Modern Java a C++ dev has not met — sealed, record, switch patterns, and the rest →

← all lessons