JAVA / RECORDS, SEALED TYPES AND ENUMS
Enum constants and behaviour-rich enums
Give each enum constant its own implementation through a constant class body, so per-constant behaviour becomes virtual dispatch instead of branching.
What you will learn
- Add an abstract method to an enum and implement it in each constant's class body
- Compare constants with == because each name is one permanent instance
- Override a shared concrete method only in the constants that behave differently
- Use getDeclaringClass() instead of getClass() once a constant has a body
Understanding Enum constants and behaviour-rich enums
Writing enum Step { TRIM, UPPER, REVERSE } does not declare three names, it declares three objects. The compiler turns each constant into a public static final Step field, and the JVM creates all of them once, in declaration order, when the class is initialised. Nothing else can produce a Step: the constructor is private and new is forbidden, so a Step reference points at one of those three objects or at null. That is why == is the right comparison, why Step.valueOf("UPPER") hands back the very same object as Step.UPPER, and why the compiler can reason about a switch covering all of them.
Because the constants are objects, they can differ in behaviour and not only in name. Declare a method abstract in the enum body and each constant supplies its implementation in braces after its name; javac compiles that brace block into an anonymous subclass of the enum, so Step.UPPER is really the single instance of an unnamed class extending Step. A call to step.apply(text) is then ordinary virtual dispatch, with the constant itself selecting the code. The compiler also rejects a constant that leaves an abstract member unimplemented, so a fourth constant cannot silently fall into a default branch the way it can with a switch written somewhere else.
The mental model to carry is a hierarchy with a fixed set of subtypes and exactly one instance of each, closed at compile time. Behaviour every constant must answer differently belongs in an abstract method; behaviour shared by most constants belongs in a concrete method that only the exceptions override, which keeps the exception visible at the constant that owns it. Behaviour that reaches outside the enum's own vocabulary, such as SQL fragments or UI wording, is better left to a switch in that layer, otherwise the enum starts importing half of the application.
public class BehaviourRichEnums {
enum Step {
TRIM {
@Override String apply(String s) { return s.strip(); }
},
UPPER {
@Override String apply(String s) { return s.toUpperCase(); }
},
REVERSE {
@Override String apply(String s) { return new StringBuilder(s).reverse().toString(); }
};
abstract String apply(String s);
}
public static void main(String[] args) {
String text = " kiro ";
for (Step step : Step.values()) {
System.out.println(step + ": [" + step.apply(text) + "]");
}
Step chosen = Step.valueOf("UPPER");
System.out.println("same object: " + (chosen == Step.UPPER));
System.out.println("declaring type: " + chosen.getDeclaringClass().getSimpleName());
System.out.println("runtime class is Step: " + (chosen.getClass() == Step.class));
}
}An enum constant is a single permanent instance of its type, and a class body after the constant name makes it its own subclass, so per-constant differences turn into virtual dispatch.
Worked examples
Shared default, one exception
Shows a concrete method used by most constants and overridden by the single constant that differs.
public class DefaultAndOverride {
enum Card {
VISA, MASTERCARD,
AMEX {
@Override int cvvLength() { return 4; }
};
int cvvLength() { return 3; }
}
public static void main(String[] args) {
for (Card c : Card.values()) {
System.out.println(c + " cvv=" + c.cvvLength()
+ " anonymous=" + (c.getClass() != Card.class));
}
}
}Example explained
Line 1cvvLength() is concrete, so VISA and MASTERCARD need no body at all and inherit the value 3.
Line 2AMEX carries braces, so only that constant is compiled into an anonymous subclass, which is why its getClass() differs from Card.class.
Line 3The exception lives next to the constant it belongs to, instead of in an if (card == AMEX) test elsewhere.
Line 4Adding a new card automatically gets the default 3; that is the tradeoff against an abstract method, which would force the new constant to decide.
Enum constants as comparators
Demonstrates an enum implementing an interface, with each constant providing the interface method so the constant can be passed as a strategy.
import java.util.Arrays;
import java.util.Comparator;
public class EnumStrategies {
enum WordOrder implements Comparator<String> {
ALPHABETICAL {
@Override public int compare(String a, String b) {
return a.compareTo(b);
}
},
BY_LENGTH {
@Override public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
},
LAST_LETTER {
@Override public int compare(String a, String b) {
return Character.compare(a.charAt(a.length() - 1), b.charAt(b.length() - 1));
}
}
}
public static void main(String[] args) {
for (WordOrder order : WordOrder.values()) {
String[] words = { "beetle", "cat", "emu", "dog" };
Arrays.sort(words, order);
System.out.println(order + " " + Arrays.toString(words));
}
}
}Example explained
Line 1The enum declares no compare() of its own; the inherited abstract method from Comparator is satisfied by every constant having a body, which javac requires.
Line 2compare() must be declared public in each body because an override may not be less visible than the interface method it implements.
Line 3Arrays.sort(words, order) accepts the constant directly, so a named constant now doubles as a reusable strategy object.
Line 4BY_LENGTH leaves cat, emu, dog in their original relative order because Arrays.sort on objects is stable, not because the comparator says so.
Constants that point at each other
Shows constants returning other constants to model transitions, which works because the body runs long after initialisation.
public class EnumTransitions {
enum Light {
GREEN {
@Override Light next() { return YELLOW; }
},
YELLOW {
@Override Light next() { return RED; }
},
RED {
@Override Light next() { return GREEN; }
};
abstract Light next();
}
public static void main(String[] args) {
Light light = Light.GREEN;
for (int i = 0; i < 5; i++) {
System.out.print(light + " -> ");
light = light.next();
}
System.out.println(light);
}
}Example explained
Line 1GREEN's body names YELLOW, which does not exist yet while GREEN is being constructed, but the body only executes when next() is called, so the field is already assigned by then.
Line 2The same wiring passed as a constructor argument, GREEN(YELLOW), is a compile error: an enum constructor may not read the enum's own static fields.
Line 3next() is abstract, so the cycle is complete by construction; a forgotten transition is a compile error rather than a null return.
Line 4print then println produce a single line, and the loop lands on RED after five moves from GREEN.
Important notes
A constant body is an anonymous class body: it can override methods and hold private helpers, but it cannot declare a constructor, and an override may not reduce the visibility of the method it replaces.
Constants with bodies still work in switch, EnumSet and EnumMap, but they break identity checks such as x.getClass() == Light.class, so use getDeclaringClass() when you need the enum type itself.
Common mistakes
Wiring constants through the constructor, as in GREEN(YELLOW): javac rejects it because an enum constructor cannot read the enum's static fields while they are still being assigned, so the reference has to move into a method body.
Declaring a helper only inside a constant body and then calling it on an enum-typed variable: it does not compile, because the method is a member of the anonymous subclass and not of the enum type, which must declare it abstract or concrete.
Branching on ordinal() or storing it in a database: inserting or reordering a constant shifts every number, so previously saved rows silently change meaning; store name() and read it back with valueOf.
Try it yourself
Change, predict, then run
Declare enum Bracket { LOW, MID, HIGH } with an abstract method int taxOn(int cents), where LOW returns 0, MID a fifth of the amount and HIGH two fifths. Loop over values() and print the tax on 100000 cents for each constant.
Open the Java workspaceCheck your understanding
An enum declares an abstract method apply, and its three existing constants each have a class body. A developer adds a fourth constant, SLUGIFY, with no body. What is the result?
- Compilation fails, because an enum with an abstract member requires every constant to supply an implementation in its own body
- It compiles, and calling apply on SLUGIFY throws AbstractMethodError at runtime
- It compiles, and SLUGIFY inherits the implementation from the constant declared just before it
- It compiles only if SLUGIFY is declared last, since the constants are created in declaration order
Show answer
An enum with an abstract member is only legal when every constant carries a body implementing it, so the missing body is rejected at the declaration; the constant could never be created otherwise. AbstractMethodError sounds plausible because that is what a missing implementation causes when separately compiled classes drift apart, but here the constants are compiled together with the enum, so the gap cannot survive compilation. Bodies are also never shared: each one becomes its own anonymous subclass, so nothing is inherited from a neighbouring constant.