RizTech Academy logo
RizTech Academy
Object-Oriented JavaLesson 7 of 820 min

Enums and why they beat constants

The strings lesson listed one thing String is not for: a fixed set of options. "veg", "jain", "student". A typo in one of those compiles happily and fails at runtime, or worse, silently takes the default branch.

An enum is a type whose values are a closed, named set. A typo in an enum constant does not compile. That single property is most of the argument.

The problem with strings

String plan = "vegg";                     // compiles
if (plan.equals("veg")) { ... }           // never true, no error anywhere

Nothing in the type system knows that plan is supposed to be one of four things. Every method taking it has to guess, validate, or trust.

enum Plan { VEG, JAIN, STUDENT, TRIAL }
Plan plan = Plan.VEGG;   // error: cannot find symbol

The mistake is now a build failure. Push errors from runtime to compile time whenever the language lets you — it is the single most valuable habit static typing offers, and enums are the cheapest place to practise it.

The free machinery

Plan p = Plan.STUDENT;

System.out.println(p);
System.out.println(p.name() + " ordinal=" + p.ordinal());
System.out.println(Arrays.toString(Plan.values()));
System.out.println(Plan.valueOf("JAIN"));
STUDENT
STUDENT ordinal=2
[VEG, JAIN, STUDENT, TRIAL]
JAIN
Member Gives
values() An array of every constant, in declaration order
valueOf("JAIN") The constant with that exact name
name() The constant's name as declared
ordinal() Its position, from 0
toString() The name, unless you override it
compareTo Ordering by ordinal()

valueOf is case-sensitive and throws on anything unknown:

threw: No enum constant Plan.veg

That message is worth recognising. It nearly always means data from a file, a database or an API has arrived in a case or spelling your enum does not use — which is why a parser usually wants a static factory that tries valueOf(s.strip().toUpperCase()) and fails with a message naming the field.

Two more properties, both useful:

  • Enum constants are singletons, so == is safe and correct for them. You do not need .equals(), and == is null-safe, which .equals() is not.
  • Enums can be used in switch without qualifying the constant: case VEG ->, not case Plan.VEG ->.

Constants can carry data

This is where enums stop being a tidy list and start being useful.

enum Plan {
    VEG("Vegetarian", 8_235),
    JAIN("Jain (no onion)", 9_100),
    STUDENT("Student", 7_412),
    TRIAL("Trial", 0);

    private final String label;
    private final int pricePaise;

    Plan(String label, int pricePaise) {
        this.label = label;
        this.pricePaise = pricePaise;
    }

    String label() { return label; }
    int pricePaise() { return pricePaise; }
}
VEG      Vegetarian         Rs 82.35
JAIN     Jain (no onion)    Rs 91.00
STUDENT  Student            Rs 74.12
TRIAL    Trial              Rs 0.00

The constructor is implicitly private — you cannot create new constants. The semicolon after the last constant is required once anything follows it, and forgetting it produces a baffling error.

A price list that lives with the type it describes cannot drift out of sync with it. The alternative — an enum plus a Map<Plan, Integer> of prices somewhere else — has two places to update and one of them gets forgotten.

Behaviour per constant

A constant can override a method, which gives you a state machine in a dozen lines:

enum Status {
    ORDERED          { @Override Status next() { return COOKING; } },
    COOKING          { @Override Status next() { return OUT_FOR_DELIVERY; } },
    OUT_FOR_DELIVERY { @Override Status next() { return DELIVERED; } },
    DELIVERED        { @Override Status next() { return DELIVERED; } };

    abstract Status next();
}
ORDERED -> next: COOKING
COOKING -> next: OUT_FOR_DELIVERY
OUT_FOR_DELIVERY -> next: DELIVERED
DELIVERED -> next: DELIVERED

The compiler forces every constant to supply next(). Add CANCELLED later and the build fails until you have decided what it does — which is precisely the reminder you want.

For two or three constants this is elegant. For a dozen it becomes hard to read, and a switch inside a normal method is clearer. Judge by how much code each constant contributes.

Exhaustive switch: the payoff

static String describe(Plan plan) {
    return switch (plan) {
        case VEG -> "standard vegetarian";
        case JAIN -> "no onion or garlic";
        case STUDENT -> "10% off with an ID card";
        case TRIAL -> "three days free";
    };
}

No default, and that is deliberate. Remove one case and the build fails:

error: the switch expression does not cover all possible input values
        return switch (plan) {
               ^

Now add a fifth plan to the enum. Every switch that does not handle it stops compiling, and the compiler has just written your to-do list.

Add a default -> "unknown" and you throw that away: the new plan silently becomes "unknown" everywhere, in production, with no warning. Omit default on an enum switch unless you genuinely want unlisted values ignored. This is the best argument in the language for using enums at all, and the sealed types lesson extends it to whole hierarchies.

EnumMap and EnumSet

Purpose-built collections, and worth knowing they exist:

Map<Plan, Integer> counts = new EnumMap<>(Plan.class);
counts.put(Plan.JAIN, 3);
counts.put(Plan.VEG, 12);
System.out.println(counts);

EnumSet<Plan> discounted = EnumSet.of(Plan.STUDENT, Plan.TRIAL);
System.out.println(discounted + " contains VEG? " + discounted.contains(Plan.VEG));
{VEG=12, JAIN=3}
[STUDENT, TRIAL] contains VEG? false

Both are backed by arrays and bit vectors rather than hashing, so they are faster and smaller than HashMap and HashSet — and note the EnumMap printed in declaration order, not insertion order, which is usually what you want for a report.

The trap: never persist ordinal()

ordinal() is a position, and positions move.

enum Plan { VEG, JAIN, STUDENT, TRIAL }

STUDENT is 2. Store a 2 in your database, then a colleague alphabetises the enum:

enum Plan { JAIN, STUDENT, TRIAL, VEG }

Every stored 2 now means TRIAL. No exception, no migration, no warning — every historical record quietly changed meaning, and a free trial is now being billed at zero.

Persist name(), never ordinal(). Renaming a constant is then a visible, searchable change instead of a silent one. The same goes for serialising to JSON or sending over an API: Jackson uses the name by default, and it is right to.

ordinal() is fine for what it is for — ordering within a single run, indexing an array inside one class. It is never a durable identifier.

When not to use an enum

  • When the set is not fixed. Cities you deliver to, product categories a shop edits. If adding a value means a code deploy, an enum is wrong — that is data, and belongs in a database.
  • When the values carry a lot of behaviour each. Four constants each with sixty lines of override is a sealed interface with four records, which is the next lesson.
  • As a replacement for boolean on a two-value flag where the names add nothing. enum OnOff { ON, OFF } for a field called enabled is ceremony. Though enum Visibility { PUBLIC, PRIVATE } in a method signature genuinely beats a bare true, because setVisibility(true) is unreadable at the call site.

Check your work

What does an enum give you that a String constant does not? A typo becomes a compile error rather than a runtime surprise; the set of valid values is part of the type; and switch can be checked for exhaustiveness.

Why is == safe on enum constants? Each constant is a singleton, so identity and equality coincide. It is also null-safe, unlike .equals().

What does valueOf("veg") do? Throws IllegalArgumentException: No enum constant Plan.veg. It is case-sensitive and matches the declared name exactly.

Why omit default from a switch over an enum? So that adding a constant breaks every switch that does not handle it. With a default, the new value is silently swallowed in production.

What is wrong with storing ordinal()? It is a position. Reordering or inserting a constant silently changes the meaning of every stored value. Store name().

When is an enum the wrong tool? When the set of values changes without a code deploy — that is data. Or when each constant carries substantial behaviour, which is a sealed hierarchy.

Practice 2, the price list. Putting pricePaise on the enum means there is exactly one place to change a price, and no way to have a plan with no price. The Map<Plan, Integer> version compiles fine with a missing entry and returns null, which unboxes to a NullPointerException somewhere unrelated.

Practice 4, adding a constant. After adding KETO to Plan, every switch without a default fails to compile, naming each file and line. That is the feature. With a default -> "unknown" in place, the code compiles and every keto customer is described as "unknown" until somebody notices — which is the same class of bug as the ordinal() one: silent, and discovered by a customer.

Practice 6, parsing safely.

static Plan parse(String raw) {
    if (raw == null || raw.isBlank()) {
        throw new IllegalArgumentException("plan must not be blank");
    }
    try {
        return Plan.valueOf(raw.strip().toUpperCase());
    } catch (IllegalArgumentException e) {
        throw new IllegalArgumentException(
                "unknown plan [" + raw + "], expected one of " + Arrays.toString(Plan.values()));
    }
}

Listing the valid values in the message is the difference between a bug report and a fix. The caller pasted Veg with a trailing space; now they can see that.

Practice

  1. Replace strings with an enum. Take a program using "veg", "jain" and "student" and convert it. Then try to use a misspelled constant and confirm it will not compile.

  2. Put the price on the enum. Give each constant a label and a price in paise, with accessors, and print a price list by looping over values(). Then write the Map<Plan, Integer> version, leave one plan out of the map, and see how far the program gets before failing.

  3. Write a state machine. Status with ORDERED, COOKING, OUT_FOR_DELIVERY, DELIVERED and a next() per constant. Then add CANCELLED and let the compiler tell you what you have not decided.

  4. Prove the exhaustiveness payoff. Write a switch over Plan with no default, then add a fifth constant and read the compile error. Then add a default, add a sixth constant, and notice that nothing at all happens.

  5. Break ordinal() on purpose. Print the ordinals, write one down as if storing it, reorder the constants, and print what that stored number now means. One sentence on what would have happened in production.

  6. Harder — parse a CSV column. Write Plan.parse(String) handling "veg", " VEG " and "Veg", and throwing a message that names the bad value and lists the valid ones. Then feed it a file where one row in twenty is wrong, and report every bad row with its line number instead of stopping at the first.

Next: sealed types and pattern matching — the Java 21 feature that gives you the enum's exhaustiveness for whole families of classes.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship