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 class | Java enum |
|---|---|
| A distinct integral type with named values | A final class extending java.lang.Enum, with a fixed instance set |
static_cast<int>(Coin::QUARTER) is 25 | Coin.QUARTER.minorValue() is 25; ordinal() is 2 |
Underlying type declarable: : int, : uint8_t | No underlying type. A constant is a reference |
| Cannot hold data or methods | Fields, methods, constructors, interfaces |
| No mapping from a name to a value at run time | name(), valueOf(String), values(), toString() |
Coin::QUARTER in a case label | case QUARTER bare, or case Coin.QUARTER since Java 21 |
switch on an enum warns under -Wswitch | Only a switch expression errors; a statement is silent |
Free function + switch for per-constant behaviour | A method body on the constant itself |
int counts[COIN_COUNT] with a sentinel constant | EnumMap<Coin, Integer>, and never a sentinel |
| Can be forward declared with its underlying type | No 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:
- Iteration is in declaration order, documented, not incidental. Block 5 inserts
[QUARTER, NICKEL, DIME]and iterates[NICKEL, DIME, QUARTER]. Every log line, everytoString, every test assertion over the key set is now reproducible. - A null key throws immediately, with
Cannot invoke "Object.getClass()" because "key" is null. AHashMapaccepts null as a key and hides the mistake until something reads it back. - The key space is known, so
counts.size() == Coin.values().lengthis a real completeness check.mentionsEveryCoin()is that line, and it returnsfalsein 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:
- 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.
- 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.
- 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:
| Test | The allows body version | The reference's table |
|---|---|---|
| Intrinsic | Fails. A machine's legal moves are a rule someone wrote, and rules change per deployment | |
| Closed | Fails. MaintenanceTransitions.all() is a second rule set for the same states | |
| Small | Passes, 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 linesworked/src/CoinPurse.java54 linesworked/src/LooseCoinPurse.java42 linesworked/src/MachineEvent.java10 linesworked/src/MachineState.java55 linesworked/src/Main.java146 lines
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.