Topic 4.10
Enums
In one line
An enum is a class with a fixed, named set of instances, such as SMALL, MEDIUM, LARGE. Enums replace error-prone int and String constants with a real type the compiler checks, and they can carry fields, constructors and behaviour.
Think of it like this
The sizes on a coffee-shop menu: Small, Medium, Large. You can't order a "Medium-ish" or a size 7; the menu is a fixed list. Each size also has facts attached (Small is 200 ml and ₹120). An enum is that menu, written as a type: only the listed values exist, and each can carry its own data.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Enum
- Short for enumeration: a type whose values are a fixed list of named constants, like the days of the week.
- Enum constant
- One of the named values in an enum, such as
MONDAY. Each is a single object that exists exactly once. - Ordinal
- A constant's position in the declaration, starting at 0.
SMALL.ordinal()is 0. - `values()`
- A static method every enum has. It returns an array with all the constants, in the order they were declared.
- `valueOf(String)`
- A static method that returns the constant with exactly that name, or throws
IllegalArgumentExceptionif there's none. - Constant-specific body
- A
{ ... }block after a constant that gives that one constant its own version of a method. - `EnumMap` / `EnumSet`
- A map and a set specialised for enum keys. They're fast, compact and keep declaration order.
- Exhaustive
- Covering every possible value. A switch expression over an enum must handle every constant (or have a
default).
Step by step
01Declare an enum and use it
List the constants, separated by commas, in UPPER_SNAKE_CASE. Refer to them through the type: Size.MEDIUM.
A variable of type Size can hold only SMALL, MEDIUM, LARGE or null. A method int price(Size s) can't be called with 42 or "medium"; the compiler rejects it.
enum Size { SMALL, MEDIUM, LARGE }
Size s = Size.MEDIUM;
System.out.println(s); // MEDIUM
System.out.println(s.ordinal()); // 1
System.out.println(s == Size.MEDIUM); // true, == is the right way02What the compiler actually makes
An enum compiles to a final class (unless some constant has a body) extending java.lang.Enum<Size>. Each constant is a public static final Size field. A static initialiser creates the three objects, passing each one's name and ordinal to the Enum constructor, and stores them in a hidden $VALUES array.
values() returns $VALUES.clone(), a fresh copy each call, so callers can't corrupt the real array. In a hot loop, cache it in a static final field.
public final class Size extends java.lang.Enum<Size> {
public static final Size SMALL = new Size("SMALL", 0);
public static final Size MEDIUM = new Size("MEDIUM", 1);
public static final Size LARGE = new Size("LARGE", 2);
private static final Size[] $VALUES = { SMALL, MEDIUM, LARGE };
private Size(String name, int ordinal) { super(name, ordinal); }
public static Size[] values() { return $VALUES.clone(); }
public static Size valueOf(String name) { return Enum.valueOf(Size.class, name); }
}03Give constants data with fields and a constructor
Put arguments after each constant, end the constant list with a semicolon, then declare fields and a constructor. The constructor is implicitly private, and it runs once for each constant during class initialisation.
Now the facts about each size live with the size. Adding EXTRA_LARGE(500, 220) is one line, and every method that uses ml() or price() works without change.
enum Size {
SMALL(200, 120), MEDIUM(300, 150), LARGE(400, 180); // semicolon needed
private final int ml;
private final int price;
Size(int ml, int price) { // implicitly private
this.ml = ml;
this.price = price;
}
int ml() { return ml; }
int price() { return price; }
}04Switch over an enum
Inside a switch on an enum, you write the bare constant names (case SMALL), not Size.SMALL (since Java 21 the qualified form is also allowed).
A switch expression must be exhaustive. Cover all constants and you need no default. Leave one out and javac says the switch expression does not cover all possible input values. That's a gift: add a constant later and the compiler lists every switch to update. A default branch would hide that, so avoid it when you can list the cases.
05Behaviour per constant
Declare an abstract method in the enum and give each constant a body that implements it. Each constant becomes an instance of its own anonymous subclass.
This replaces a switch that you'd otherwise repeat in many places: the behaviour is defined right next to the constant, and the compiler forces every new constant to implement it. It's the Strategy pattern built into the language (see the System Design course's design patterns).
enum Op {
PLUS("+") { int apply(int a, int b) { return a + b; } },
MINUS("-") { int apply(int a, int b) { return a - b; } },
TIMES("*") { int apply(int a, int b) { return a * b; } };
final String symbol;
Op(String symbol) { this.symbol = symbol; }
abstract int apply(int a, int b);
}06Name, ordinal and what to store
ordinal() is just the position in the source. Insert a constant in the middle and every later ordinal shifts. Never store ordinals in a database or file; store name() (or, better, an explicit code field you control), and read it back with valueOf or a lookup map.
valueOf is case-sensitive and strict: Size.valueOf("small") throws IllegalArgumentException: No enum constant Size.small. When parsing user input, normalise first (trim().toUpperCase()), or build your own lookup that returns a default.
Try it yourself
- 1
Add a constant and let the compiler help
In "Enum basics", add
EXTRA_LARGEtoSize. Compile before touching the switch: read the error, then add the missing case. Predict the first lines of output. - 2
Add an operation
In the
Opexample, addDIVIDE("/")with integer division. What does the compiler say if you forget its body? Predict7 / 3. - 3
Parse friendly input
Call
Size.fromCode("xl")in the second example inside a try/catch and print the message. Then makefromCodereturnMEDIUMas a default instead of throwing, and decide which behaviour is safer for an API.
Code & diagrams
The message uses the canonical name, Main.Size, because Size is nested in Main.
Expected output
0 SMALL
1 MEDIUM
2 LARGE
picked LARGE, == LARGE? true
SMALL before LARGE? true
LARGE is 400 ml
No enum constant Main.Size.HUGEThe static map is filled in a static block, which runs after all constants exist.
Expected output
SMALL 200 ml Rs 120 (0.60 per ml)
MEDIUM 300 ml Rs 150 (0.50 per ml)
LARGE 400 ml Rs 180 (0.45 per ml)
code ' m ' -> MEDIUMExpected output
7 + 3 = 10
7 - 3 = 4
7 * 3 = 21
uses: {PLUS=9, TIMES=4}
weekdays: [MON, TUE, WED, THU, FRI]
range: [TUE, WED, THU]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Try to create an enum constant with new
Write Size s = new Size();.
Break #2
valueOf with the wrong spelling
Call Size.valueOf("HUGE") (or "small" in lower case) without a try/catch.
Myth vs fact
Myth
Enums are just named integers, like in C.
Fact
Java enum constants are full objects of their own class, with fields, methods and a type the compiler checks. The ordinal is only a position.
Myth
You should compare enums with equals.
Fact
== is correct and preferred: each constant is a single object. It's also null-safe (x == Size.SMALL can't throw), unlike x.equals(...).
Myth
It's fine to save ordinal() in the database.
Fact
Reordering or inserting constants silently changes ordinals and corrupts stored data. Store the name or an explicit code field.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
An enum with a single constant is the simplest thread-safe, serialization-safe singleton (Effective Java Item 3): class initialisation guarantees one instance, and the JVM's enum serialization rules guarantee no copies.
Enumalso makesclone()throw. - ▸
EnumSetfor up to 64 constants is aRegularEnumSetstoring onelongbit-vector;containsis a bit test andaddAllis a bitwise OR.EnumMapis an array indexed by ordinal. Both iterate in declaration order and beat their hash-based equivalents in speed and memory. - ▸
Since Java 5,
switchon an enum compiles to atableswitchover a synthetic$SwitchMaparray (mapping ordinals to case numbers) generated in a helper class, so recompiling the enum with reordered constants doesn't break old switches. For switch expressions over enums with nodefault, javac adds a hidden default that throwsMatchException(Java 21+;IncompatibleClassChangeErrorbefore) if an unknown constant arrives from a newer enum version. - ▸
Enum constants with bodies are anonymous subclasses, so
Op.PLUS.getClass()isMain$Op$1, notMain$Op. UsegetDeclaringClass()to get the enum type. EnumhashCode()is identity-based and differs between JVM runs, soHashMapiteration order over enum keys isn't stable; useEnumMap.
Remember this
- 1
enum Size { SMALL, MEDIUM, LARGE }declares a class namedSizewith exactly three instances. Each name is apublic static finalfield holding the one and only object for that constant, created when the enum class is initialised. You can't create more:new Size()is a compile error. - 2
Before Java 5, people wrote
static final int SMALL = 0, MEDIUM = 1;. Nothing stopped a method that takes anintsize from receiving42, and printing it showed a meaningless number. With an enum, a parameter of typeSizecan only ever be one of the three constants (ornull), and printing it shows its name. - 3
Every enum gets useful methods for free:
name()(the constant's exact name),ordinal()(its position, starting at 0),values()(a new array of all constants, in declaration order),valueOf("SMALL")(look up by name, throwsIllegalArgumentExceptionfor unknown names),compareTo(by ordinal) and atoString()that returns the name unless you override it. - 4
Because each constant is a single object, you compare enums with
==. It's safe (there's never a secondSMALL), fast, and null-safe on the left. Enums work inswitch, and a switch expression over an enum must cover every constant, so adding a new constant makes the compiler point at every switch you forgot to update (Topic 2.4). - 5
Enums are full classes. They can have fields, a constructor (always private; it runs once per constant), methods, and even constant-specific bodies where each constant overrides an abstract method with its own behaviour. They can implement interfaces. They can't extend another class (they all extend
java.lang.Enum). - 6
The JDK has collections built for enums:
EnumMap(a map keyed by an enum, stored as an array indexed by ordinal) andEnumSet(a set stored as bits in along). Both are much faster and smaller thanHashMap/HashSet, and they iterate in declaration order.
Explain it without notes
Why are enums better than int or String constants?
What does the compiler generate for enum Size { SMALL, MEDIUM, LARGE }?
Why is == the right way to compare enum values?
What's the danger of ordinal() and how should you persist enums?
How do constant-specific method bodies work, and when would you use them?
Practice
Write enum Planet with MERCURY, EARTH, JUPITER and a field surfaceGravity (3.7, 9.8, 24.8). Print what a 50 kg mass weighs (mass times gravity, rounded to one decimal) on each planet.
Write enum TrafficLight { RED, GREEN, YELLOW } with a method next() implemented with a switch expression (RED -> GREEN -> YELLOW -> RED). Starting from RED, print five transitions.
Count the votes {"TEA", "COFFEE", "TEA", "JUICE", "TEA", "COFFEE"} into an EnumMap<Drink, Integer> and print it.
Trade-offs
- ↔
Enums are perfect when the set of values is known at compile time. If values come from configuration or a database and change without a release (product categories, country lists), use a class or a lookup table instead; adding an enum constant needs a recompile and redeploy.
- ↔
Constant-specific bodies keep behaviour beside the data, but heavy logic in enums makes them hard to test and couples the type to its uses. For big behaviour, have the enum implement an interface or map constants to strategy objects.
- ↔
A switch expression without
defaultgives compile-time exhaustiveness checks, but a library enum that gains a constant can still reach old compiled code at run time and throw. Inside one codebase, prefer nodefault; across library boundaries, handle the unknown case deliberately.
Done when you can
Done when you can declare enums with fields, constructors and methods.
Done when you can use
values(),valueOf,name()andordinal()correctly, and know not to persist ordinals.Done when you write exhaustive switch expressions over enums without
default.Done when you can implement per-constant behaviour with an abstract method.
Done when you reach for
EnumMapandEnumSetinstead of hash-based collections for enum keys.