Layers of Logic

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.

Core18 min read4 exercises
01

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.

02

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.

03

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);          // ACTIVE

Every 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 values

It 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.

04

Under the hood

Going deeper

What 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 final object, 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 where name(), ordinal(), compareTo() and a correct equals() come from.

Which is why == is safe here

if (status == Status.ACTIVE) { }        // correct, and preferred
if (status.equals(Status.ACTIVE)) { }   // also correct, and worse

There 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.

TypeIs == safe?
enumyes, alwaysexactly one object per constant, by construction
Stringnoliterals are pooled, others are not (Section 3.2)
Integernocached from -128 to 127 only (Section 7.3)
your own classnounless 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));    // 12

Each 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

MethodWhat it gives you
values()all constantsan array, in declaration order
valueOf("ACTIVE")name to constantthrows IllegalArgumentException if no match
name()the constant to its nameexactly as declared
ordinal()position in the liststarting at 0. Handle with care.
compareTo()orderingby 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.

05

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.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. An enum looks like a list of names. What is each constant actually, in memory?

    Show the answer

    Each constant is a public static final object of the enum's own type, and there is exactly one of each, created when the enum class is initialized.

    So Status.ACTIVE is one object, existing once for the whole program. Writing Status.ACTIVE in 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.

  2. 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 String trap in Section 3.2 exists because two equal Strings might be one object or two, depending on how they were made. The Integer trap 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 a Status with a Direction will not compile at all.

  3. 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 a String. There is no way for the compiler to know what set you intended, so the best it can offer is a default case 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.

07

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

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Let the Compiler Find the Missing Case

Warm up·20 min·15 points

ex-8-4-a

The 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
Missing.java
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
The int version compiles perfectly and reports "unknown" for a status that is completely valid. Nothing anywhere says this is wrong.
Hint 2
Use the expression form so the compiler can check completeness: return switch (s) { case ACTIVE -> "on duty"; case RESERVE -> "standing by"; }; with no default.
Hint 3almost the answer
Adding a fourth constant breaks every exhaustive switch in the program, with a line number for each. That is the feature, not the annoyance: it is the compiler showing you every place that now has a hole.
What this is really testing

Whether you can see the difference between a bug the compiler finds and one it cannot. Same missing case, two representations, two completely different outcomes.

B

Enums Carry Data

Real work·30 min·25 points·The Registry

ex-8-4-b

Take 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
Rich.java
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
The constant list comes first, with arguments: 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
The constructor is implicitly private and you cannot make it public. Nothing outside the enum can ever create a fourth constant, which is what guarantees there is exactly one object per constant.
Hint 3almost the answer
What you removed: every if statement anywhere in the program that asked "is this status deployable". The rule now lives with the status itself, in one place, and adding a fourth status means adding one line rather than hunting for every if.
What this is really testing

Whether you can move a rule out of scattered if statements and into the type it belongs to. This is where enums stop being a nicer int and start being genuinely useful.

C

Never Store ordinal()

Real work·25 min·25 points

ex-8-4-c

Simulate 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
Ordinal.java
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
Insert 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
Every ordinal after the insertion point shifted by one. The stored number 2 used to mean RETIRED and now means RESERVE, so every saved row silently means something different.
Hint 3almost the answer
Store 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.
What this is really testing

Whether you can see why ordinal() is a trap. It looks like a stable identifier, it is a position in a list, and the gap between those two costs companies real money when somebody reorders the constants.

D

Behaviour Per Constant

Hard·30 min·30 points

ex-8-4-d

Two 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
PerConstant.java
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
The shape is 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
Each constant is an anonymous subclass of the enum. That is Section 7.4, appearing in a place you would not have predicted, and it is why the syntax looks the way it does.
Hint 3almost the answer
Adding DIVIDE to the switch version compiles immediately if the switch has a default, or fails helpfully if it does not. Adding it to the abstract version cannot compile until you supply the body. The compiler asks you for the behaviour rather than trusting you to remember.
What this is really testing

Whether you can use the constant-specific body form, and see that it is Section 7.4 appearing somewhere unexpected. It also removes a whole class of forgetting-to-update bugs.

08

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