8.4
Enums
A type with a fixed set of named values, where the compiler refuses to let you invent a new one. It is the only place in Java where the compiler will tell you that you forgot a case.
Previously on
Section 6.4 gave you static final for constants, and told you the two keywords would combine into something new in this section.
Section 3.2 gave you switch, and noted that switching over an enum is the one case where the compiler can tell you that you missed a possibility. That claim gets paid off here.
And Section 8.3 taught you why == is dangerous on objects. Enums are the exception, and the reason is worth knowing.
The problem
Your Registry unit has a status. Try to represent it with what you have.
Attempt one: int constants.
public static final int STATUS_ACTIVE = 1;
public static final int STATUS_RESERVE = 2;
public static final int STATUS_RETIRED = 3;
unit.setStatus(7); // compiles. 7 is not a status.
unit.setStatus(-1); // compiles.
unit.setStatus(someAge); // compiles. It is an int, and so is age.Nothing is checked. Any int fits, so any bug fits. And when you print it you get 2, which means nothing to anyone reading a log.
Attempt two: String constants.
unit.setStatus("ACTIVE");
unit.setStatus("active"); // different string. Silently no match.
unit.setStatus("ACITVE"); // typo. Compiles. Fails at run time, or never.At least it prints readably. Now you have typos and case sensitivity instead, and comparison brings back the == trap from Section 3.2.
And both share the worst problem.
switch (status) {
case STATUS_ACTIVE: ...
case STATUS_RESERVE: ...
// STATUS_RETIRED forgotten
}The compiler cannot help. It has no idea that three is the complete set, because an int has four billion values and a String has infinitely many.
What you need is a type where the compiler knows every possible value.
The idea
public enum Status {
ACTIVE, RESERVE, RETIRED
}Status s = Status.ACTIVE;
unit.setStatus(Status.ACTIVE);
unit.setStatus(7); // will not compile
unit.setStatus("ACTIVE"); // will not compile
unit.setStatus(Status.ACITVE); // will not compile. The typo is caught immediately.
System.out.println(s); // ACTIVEEvery problem above is gone, and one of them is gone in a way nothing else in Java offers.
String describe(Status s) {
return switch (s) {
case ACTIVE -> "on duty";
case RESERVE -> "standing by";
// RETIRED forgotten
};
}That does not compile. The compiler says:
the switch expression does not cover all possible input valuesIt knows there are exactly three, because you told it. Add a fourth constant next year and every switch expression in your program that does not handle it becomes a compile error, with a line number.
Under the hood
Going deeperWhat an enum really is
An enum is a class. Each constant is an object.
public enum Status { ACTIVE, RESERVE, RETIRED }Java turns that into roughly this:
public final class Status extends Enum<Status> {
public static final Status ACTIVE = new Status("ACTIVE", 0);
public static final Status RESERVE = new Status("RESERVE", 1);
public static final Status RETIRED = new Status("RETIRED", 2);
private Status(String name, int ordinal) { ... } // private: nothing else can create one
}Everything follows from that.
- Each constant is a
public static finalobject, created during enum class initialization. This is the pairing from Section 6.4. - The class is
final, so nothing can extend it. - The constructor is
private, so nothing can create a fourth instance. - It extends
java.lang.Enum, which is wherename(),ordinal(),compareTo()and a correctequals()come from.
Which is why == is safe here
if (status == Status.ACTIVE) { } // correct, and preferred
if (status.equals(Status.ACTIVE)) { } // also correct, and worseThere is one object per constant, guaranteed by the language. Comparing reference identity gives the right answer by construction, not by luck.
== is also better than equals here, for two reasons. It cannot throw a NullPointerException when status is null. And it is type checked, so comparing a Status with a Direction will not compile. equals would happily return false.
| Type | Is == safe? | |
|---|---|---|
| enum | yes, always | exactly one object per constant, by construction |
| String | no | literals are pooled, others are not (Section 3.2) |
| Integer | no | cached from -128 to 127 only (Section 7.3) |
| your own class | no | unless you mean "same object" |
Enums can carry data and behaviour
This is where enums become powerful, and where most people stop learning about them.
public enum Status {
ACTIVE("On duty", true, 1.0),
RESERVE("Standing by", true, 0.5),
RETIRED("Out of service", false, 0.0);
private final String label;
private final boolean deployable;
private final double readinessFactor;
Status(String label, boolean deployable, double readinessFactor) {
this.label = label;
this.deployable = deployable;
this.readinessFactor = readinessFactor;
}
public String getLabel() { return label; }
public boolean isDeployable() { return deployable; }
public double effective(double r) { return r * readinessFactor; }
}if (unit.getStatus().isDeployable()) { }Look at what that removed. The rule about which statuses can deploy is no longer scattered across if statements in five files. It lives with the status, once.
Each constant can behave differently
You can go further and give a constant its own implementation:
public enum Operation {
PLUS { public int apply(int a, int b) { return a + b; } },
MINUS { public int apply(int a, int b) { return a - b; } },
TIMES { public int apply(int a, int b) { return a * b; } };
public abstract int apply(int a, int b);
}System.out.println(Operation.TIMES.apply(3, 4)); // 12Each constant is an anonymous subclass, which is Section 7.4 appearing where you would not expect it. This is often cleaner than a switch, because adding a constant forces you to supply the behaviour rather than remembering to update a switch elsewhere.
The built-in methods
| Method | What it gives you | |
|---|---|---|
| values() | all constants | an array, in declaration order |
| valueOf("ACTIVE") | name to constant | throws IllegalArgumentException if no match |
| name() | the constant to its name | exactly as declared |
| ordinal() | position in the list | starting at 0. Handle with care. |
| compareTo() | ordering | by ordinal, so declaration order |
for (Status s : Status.values()) {
System.out.println(s + ": " + s.getLabel());
}EnumMap and EnumSet
When the key of a map is an enum, Java has a much better implementation than HashMap.
Map<Status, Integer> counts = new EnumMap<>(Status.class);EnumMap is an array, indexed by ordinal. No hashing, no buckets, no collisions, and it uses less memory. EnumSet is a bit vector, one bit per constant, exactly the flags idea from the Section 3.1 exercises.
Both are faster and smaller than the general purpose versions. Phase X covers them properly, but the rule is simple: if your key is an enum, use EnumMap.
The singleton, done right
public enum Registry {
INSTANCE;
private final Map<Integer, String> units = new HashMap<>();
public void register(int id, String name) { units.put(id, name); }
}Registry.INSTANCE.register(101, "Atlas");Most people consider this the best way to write a singleton in Java. Exactly one instance, guaranteed by the language rather than by careful coding. It is safe when several threads start at once, and it cannot be broken by serialization or reflection, both of which defeat the hand-written versions.
What it costs
Enums are fixed at compile time. You cannot add a constant while the program runs. If your set of values comes from a database or a config file, an enum is the wrong tool.
They are heavier than an int. Each constant is a real object with a header, from Section 6.3. For a handful of constants that is nothing. For a type with thousands, think about it.
Adding a constant can break compilation elsewhere, including in code you do not own. Usually that is exactly what you want, and it is still a breaking change for anyone depending on your library.
ordinal() sits there looking like a stable id. It is not, and storing one will cost somebody an afternoon.
Enums also cannot extend a class, since they already extend Enum and Java allows one parent. They can implement interfaces, which covers most of what you would want.
What you get is a type the compiler understands completely. Invalid values cannot be written. Missing cases are caught before the program runs. Printing is readable, == is safe, and behaviour lives with the data. For a fixed set of values there is nothing better. Reaching for int constants instead is one of the clearest signs of code written before the author knew this.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
An enum looks like a list of names. What is each constant actually, in memory?
Show the answer
Each constant is a
public static finalobject of the enum's own type, and there is exactly one of each, created when the enum class is initialized.So
Status.ACTIVEis one object, existing once for the whole program. WritingStatus.ACTIVEin two places gives you the same object both times.That is why
==is safe on enums, unlike Strings and boxed Integers. There is only one object per constant, so comparing reference identity gives the right answer by construction rather than by luck.It also explains why an enum can have fields, a constructor and methods. It is a class, with a fixed set of instances and no way to make more.
Why can you use `==` on enums when it is a trap for Strings and boxed Integers?
Show the answer
Because an enum has one object per constant, guaranteed by the language.
The
Stringtrap in Section 3.2 exists because two equal Strings might be one object or two, depending on how they were made. TheIntegertrap in Section 7.3 exists because of a cache with a boundary.Neither uncertainty exists for enums. The constructor is private, nothing can call
new, and each constant is created exactly once.==is also better than.equals()here: it cannot throw a NullPointerException, and it is checked at compile time, so comparing aStatuswith aDirectionwill not compile at all.You switch over an enum and forget one constant. What happens, and how is that different from switching over an int?
Show the answer
With the modern arrow switch used as an expression, it does not compile. The compiler knows every possible value of an enum, so it can tell you exactly which one you missed.
That is impossible for an
int, which has four billion possible values, and impossible for aString. There is no way for the compiler to know what set you intended, so the best it can offer is adefaultcase that silently swallows whatever you forgot.Here is the strongest single argument for enums. Add a new constant a year later, recompile, and the compiler shows you every switch that now has a hole. With int constants you find out in production.
Exercises
Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.
4 exercises95 pointsabout 105 minutes
Let the Compiler Find the Missing Case
ex-8-4-aThe same missing case, written two ways. One compiles and reports the wrong answer for valid input. The other refuses to compile and tells you exactly what you missed.
Run the int version first, including the invalid values, and notice that nothing distinguishes a genuine status you forgot from garbage that should never have arrived.
Then write the enum version and read the compile error carefully. Note that it names the constant.
Finish with the part that matters most in real work. Add a fourth constant a year after writing the code, and watch every switch that needs updating turn into a compile error with a line number.
With int constants, that year-later change produces no errors at all, and you find out from a user.
What your program must do
- Run the int version and note what happens for RETIRED and for invalid values
- Write the enum version using an arrow switch expression, deliberately missing RETIRED
- Record the exact compile error
- Add a fourth constant to the enum and see what else stops compiling
public class Missing {
// The int version
static final int ACTIVE = 1, RESERVE = 2, RETIRED = 3;
static String describeInt(int status) {
switch (status) {
case ACTIVE: return "on duty";
case RESERVE: return "standing by";
default: return "unknown";
}
}
// TODO: the enum version
// enum Status { ACTIVE, RESERVE, RETIRED }
// static String describeEnum(Status s) { ... }
public static void main(String[] args) {
System.out.println(describeInt(RETIRED)); // what does this say?
System.out.println(describeInt(7)); // and this?
System.out.println(describeInt(-1)); // and this?
}
}
Hint 1
Hint 2
return switch (s) { case ACTIVE -> "on duty"; case RESERVE -> "standing by"; }; with no default.Hint 3almost the answer
Enums Carry Data
ex-8-4-bTake three rules that were scattered across the program and move all three into the type they describe.
Give each constant a label, a flag and a factor, with a constructor to set them. Watch the if chains disappear from everywhere else.
Do not forget the semicolon after the constant list. It is required as soon as an enum has fields, and the error you get without it does not point at the missing semicolon.
Finish by answering the last question properly. Name what you deleted from the rest of the program, and say what would happen now if somebody added a fourth status. That comparison is the argument for doing this.
What your program must do
- Give each constant a label, a deployable flag and a readiness factor
- Add a private constructor and getters
- Print a table of all statuses using values()
- Explain what moving the rules into the enum removed from the rest of the program
public class Rich {
// Before: the rules are scattered
// if (status == 1) label = "On duty";
// if (status == 1 || status == 2) canDeploy = true;
// if (status == 1) factor = 1.0; else if (status == 2) factor = 0.5; else factor = 0.0;
enum Status {
// TODO: ACTIVE, RESERVE, RETIRED, each with a label, a deployable flag,
// and a readiness factor. Add a constructor, fields and methods.
}
public static void main(String[] args) {
// TODO: print a table of every status using Status.values()
// TODO: compute effective readiness for a unit at 80.0 in each status
}
}
Hint 1
ACTIVE("On duty", true, 1.0),. The list must end with a semicolon before the fields, which is easy to forget and gives a confusing error.Hint 2
Hint 3almost the answer
Never Store ordinal()
ex-8-4-cSimulate a database, save some enum values two different ways, then reorder the enum.
Insert the new constant in the middle, not at the end. Adding at the end is the case that happens to work, which is exactly why people believe ordinals are safe until the day somebody inserts one.
Run it again and read the mismatches. Every stored row after the insertion point now means something different, and nothing failed, and nothing warned anybody.
This is a real production incident shape. It usually surfaces weeks later as “some units are showing the wrong status”, long after the change that caused it.
Write down what you would store in a real system, and one sentence on why.
What your program must do
- Run it and confirm both approaches agree
- Insert a new constant in the middle of the enum and run again
- Explain why one approach broke and the other did not
- Say what you would store in a real database, and why
import java.util.*;
public class Ordinal {
enum Status { ACTIVE, RESERVE, RETIRED }
// Pretend this is a database table
static Map<Integer, Integer> savedByOrdinal = new HashMap<>(); // unitId -> ordinal
static Map<Integer, String> savedByName = new HashMap<>(); // unitId -> name()
static void save(int unitId, Status s) {
savedByOrdinal.put(unitId, s.ordinal());
savedByName.put(unitId, s.name());
}
static void loadAll() {
for (int id : savedByOrdinal.keySet()) {
Status fromOrdinal = Status.values()[savedByOrdinal.get(id)];
Status fromName = Status.valueOf(savedByName.get(id));
System.out.printf("unit %d ordinal->%-8s name->%-8s %s%n",
id, fromOrdinal, fromName, fromOrdinal == fromName ? "" : " MISMATCH");
}
}
public static void main(String[] args) {
save(101, Status.ACTIVE);
save(102, Status.RETIRED);
loadAll();
// TODO: now insert PENDING between ACTIVE and RESERVE, recompile, and run loadAll again
}
}
Hint 1
PENDING as the second constant: enum Status { ACTIVE, PENDING, RESERVE, RETIRED }. Do not add it at the end, because adding at the end hides the problem.Hint 2
Hint 3almost the answer
name(). It is stable against reordering, it survives insertions, and it is readable when somebody is looking at the database trying to work out what went wrong at 2am.Behaviour Per Constant
ex-8-4-dTwo ways to give each enum constant its own behaviour. Both work. Only one of them makes it impossible to forget something.
Write the constant-specific version, and notice as you do that each constant body is an anonymous subclass. That is Section 7.4 showing up in a place nobody expects, and it explains the syntax.
Then add DIVIDE to both versions and pay attention to what each one made you do.
One version compiled straight away and left you to remember. The other refused to compile until you supplied the behaviour, in the same place as everything else about that constant.
Say which you would ship. There is a defensible answer either way, and the reasoning is what matters.
What your program must do
- Implement the constant specific version with an abstract method
- Confirm both versions give identical results
- Add DIVIDE to both, and record what each one forced you to do
- Explain which version you would ship, and why
public class PerConstant {
// Version 1: a switch. Works, and has a hole waiting.
enum OpSwitch {
PLUS, MINUS, TIMES;
int apply(int a, int b) {
return switch (this) {
case PLUS -> a + b;
case MINUS -> a - b;
case TIMES -> a * b;
};
}
}
// Version 2: TODO write it with constant specific bodies and an abstract method
// enum Op {
// PLUS { ... },
// ...
// public abstract int apply(int a, int b);
// }
public static void main(String[] args) {
for (OpSwitch op : OpSwitch.values())
System.out.println(op + "(6,3) = " + op.apply(6, 3));
// TODO: same loop for your Op
// TODO: add DIVIDE to BOTH versions and note what each one made you do
}
}
Hint 1
PLUS { public int apply(int a, int b) { return a + b; } }, for each constant, then public abstract int apply(int a, int b); after the semicolon.Hint 2
Hint 3almost the answer
After the credits
Enums keep showing up, and always as the better option.
In Phase X, EnumMap and EnumSet can be smaller and faster than general-purpose maps and sets for enum keys. They skip general hashing. An EnumMap uses ordinal-based indexed storage, which connects to the direct array access from Section 4.2.
In Phase XII, exception handling uses enums for error categories, because a fixed set of failure kinds is exactly what an enum is for.
In Phase XIV, the enum singleton becomes the recommended pattern, because the language guarantees a single instance even when many threads start at once. The hand-written alternatives all have subtle holes, and this one does not.
One thing is still missing from your identity toolkit. equals() from Section 8.3 answers “are these the same?”. Enums also answer “which comes first?”, through compareTo(), which they inherit for free.
Your own classes do not get that for free. Sorting a list of Student objects needs an order, and that means Comparable, in Phase X. It has its own contract, and its own way of failing quietly when you break it.
Threads you opened in this section
- enumAn enum is the safest way to write a singleton in Java.Phase XIV. Concurrency
enum will return in Phase X. The Collections Framework