Lesson 10 of 71
Enums and Annotations
An enum is a class with a fixed set of named instances. An annotation is metadata attached to a declaration that tools, the compiler, or the program itself can read. After this lesson the reader can write enums with behavior, choose EnumSet and EnumMap over general collections, and declare and read custom annotations.
| Topic | API or keyword |
|---|---|
| Enum basics | enum, values, valueOf, ordinal, name |
| Enum behavior | fields, constructors, constant-specific bodies |
| Enum collections | EnumSet (bit vector), EnumMap (array) |
| Annotations | @interface, @Retention, @Target, @Inherited, @Repeatable |
| Reading annotations | isAnnotationPresent, getAnnotation, getAnnotationsByType |
Example 1: the smallest enum and its generated methods
Each constant of an enum is a public static final field holding the single instance for that name. The compiler adds a static values() and valueOf(String) to every enum.
public class EnumBasics {
enum Side { BUY, SELL }
public static void main(String[] args) {
for (Side s : Side.values()) {
System.out.println(s.name() + " ordinal=" + s.ordinal());
}
Side parsed = Side.valueOf("SELL");
System.out.println(parsed == Side.SELL);
System.out.println(Side.BUY.compareTo(Side.SELL) < 0);
try {
Side.valueOf("sell");
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentException: " + e.getMessage());
}
}
}BUY ordinal=0
SELL ordinal=1
true
true
IllegalArgumentException: No enum constant EnumBasics.Side.sellFacts shown here:
ordinal()is the declaration position, starting at 0. Reordering constants changes it, so storing ordinals in a database or a file breaks when someone inserts a constant. Storename()or an explicit code field instead.- Each constant exists once per class loader, so
==is a correct comparison and never throws onnull. valueOfis case-sensitive and throwsIllegalArgumentExceptionfor an unknown name.- Enums implement
Comparablein ordinal order and every enum extendsjava.lang.Enum, so an enum cannot extend another class.
values() returns a new array on every call, because arrays are mutable. Hot code caches it in a private static final field.
Example 2: fields, constructors, and methods
An enum constant can pass arguments to a constructor. The constructor is implicitly private; code outside the enum cannot create new instances.
import java.util.HashMap;
import java.util.Map;
public class EnumFields {
enum Currency {
USD("840", 2), JPY("392", 0), BHD("048", 3);
private static final Map<String, Currency> BY_CODE = new HashMap<>();
static {
for (Currency c : values()) BY_CODE.put(c.isoCode, c);
}
private final String isoCode;
private final int minorUnits;
Currency(String isoCode, int minorUnits) {
this.isoCode = isoCode;
this.minorUnits = minorUnits;
}
long toMinor(long major) {
long factor = 1;
for (int i = 0; i < minorUnits; i++) factor *= 10;
return major * factor;
}
static Currency fromCode(String code) {
Currency c = BY_CODE.get(code);
if (c == null) throw new IllegalArgumentException("unknown code " + code);
return c;
}
}
public static void main(String[] args) {
System.out.println(Currency.fromCode("392"));
for (Currency c : Currency.values()) {
System.out.println(c + " 5 -> " + c.toMinor(5));
}
}
}JPY
USD 5 -> 500
JPY 5 -> 5
BHD 5 -> 5000The lookup map is filled in a static block. An enum constructor cannot read a non-constant static field of its own enum, because constants are created first, before the other static fields are initialized; the compiler rejects such a read. The static block runs after all constants exist, so it can call values().
Example 3: constant-specific bodies and abstract methods
A constant can have its own class body that overrides methods. When the enum declares an abstract method, every constant must implement it, and the compiler reports a missing one.
public class ConstantBodies {
enum Op {
ADD("+") { long apply(long a, long b) { return Math.addExact(a, b); } },
SUB("-") { long apply(long a, long b) { return Math.subtractExact(a, b); } },
MUL("*") { long apply(long a, long b) { return Math.multiplyExact(a, b); } };
final String symbol;
Op(String symbol) { this.symbol = symbol; }
abstract long apply(long a, long b);
}
public static void main(String[] args) {
for (Op op : Op.values()) {
System.out.println("7 " + op.symbol + " 3 = " + op.apply(7, 3));
}
System.out.println(Op.ADD.getClass() == Op.class);
System.out.println(Op.ADD.getDeclaringClass() == Op.class);
}
}7 + 3 = 10
7 - 3 = 4
7 * 3 = 21
false
trueA constant with a body is an instance of an anonymous subclass, so getClass() returns that subclass. getDeclaringClass() always returns the enum type, which makes it the method to use when grouping constants by type.
Example 4: switch over enums
A switch expression over an enum is exhaustive when it lists every constant, so it needs no default. Adding a constant then breaks compilation at every switch that misses it, the same check that sealed types give.
public class EnumSwitch {
enum OrderType { MARKET, LIMIT, STOP }
static String describe(OrderType t) {
return switch (t) {
case MARKET -> "fill at best price";
case LIMIT -> "fill at limit or better";
case STOP -> "becomes market at trigger";
};
}
static boolean needsPrice(OrderType t) {
return switch (t) {
case LIMIT, STOP -> true;
case MARKET -> false;
};
}
public static void main(String[] args) {
for (OrderType t : OrderType.values()) {
System.out.println(t + ": " + describe(t) + ", price=" + needsPrice(t));
}
try {
describe(null);
} catch (NullPointerException e) {
System.out.println("null -> NullPointerException");
}
}
}MARKET: fill at best price, price=false
LIMIT: fill at limit or better, price=true
STOP: becomes market at trigger, price=true
null -> NullPointerExceptionCase labels use the bare constant name (LIMIT, not OrderType.LIMIT; the qualified form is also allowed since Java 21). A switch without case null throws NullPointerException on null, as pattern matching describes.
Example 5: EnumSet and EnumMap
EnumSet stores membership as bits: EnumSet.noneOf returns a RegularEnumSet backed by one long when the enum has at most 64 constants, and a JumboEnumSet backed by a long[] otherwise (read from EnumSet.java in JDK 25). EnumMap stores values in an array indexed by ordinal(). Both iterate in declaration order, and both avoid hashing.
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public class EnumCollections {
enum Permission { READ, WRITE, DELETE, ADMIN }
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
public static void main(String[] args) {
Set<Permission> user = EnumSet.of(Permission.READ, Permission.WRITE);
Set<Permission> all = EnumSet.allOf(Permission.class);
Set<Permission> missing = EnumSet.complementOf(EnumSet.copyOf(user));
System.out.println(user + " " + missing);
System.out.println(all.containsAll(user));
System.out.println(user.getClass().getSimpleName());
Set<Day> weekdays = EnumSet.range(Day.MON, Day.FRI);
System.out.println(weekdays);
Map<Day, Integer> trades = new EnumMap<>(Day.class);
trades.put(Day.WED, 12);
trades.put(Day.MON, 7);
trades.put(Day.MON, trades.get(Day.MON) + 3);
System.out.println(trades);
}
}[READ, WRITE] [DELETE, ADMIN]
true
RegularEnumSet
[MON, TUE, WED, THU, FRI]
{MON=10, WED=12}EnumMap prints in declaration order even though WED was inserted first. The table compares the choices.
| Need | General collection | Enum collection | Difference |
|---|---|---|---|
| Set of flags | HashSet<Permission> |
EnumSet<Permission> |
one long vs a hash table of nodes; containsAll is a bitwise check |
| Map keyed by enum | HashMap<Day, V> |
EnumMap<Day, V> |
array slot per constant, no hashing, sorted iteration |
Bit flags as int |
int with masks |
EnumSet |
same memory cost class, but type-safe and printable |
Example 6: enums as singletons and state machines
A one-constant enum is a singleton: the JVM creates the instance once during class initialization, serialization returns the same instance, and reflection cannot call an enum constructor. An enum also models a state machine, a fixed set of states with allowed transitions between them.
import java.util.EnumSet;
import java.util.Set;
public class EnumStateMachine {
enum IdGenerator {
INSTANCE;
private long next = 1;
long nextId() { return next++; }
}
enum OrderState {
NEW, PARTIALLY_FILLED, FILLED, CANCELLED;
Set<OrderState> next() {
return switch (this) {
case NEW -> EnumSet.of(PARTIALLY_FILLED, FILLED, CANCELLED);
case PARTIALLY_FILLED -> EnumSet.of(PARTIALLY_FILLED, FILLED, CANCELLED);
case FILLED, CANCELLED -> EnumSet.noneOf(OrderState.class);
};
}
OrderState moveTo(OrderState target) {
if (!next().contains(target)) {
throw new IllegalStateException(this + " -> " + target);
}
return target;
}
}
public static void main(String[] args) {
System.out.println("id " + IdGenerator.INSTANCE.nextId() + ", " + IdGenerator.INSTANCE.nextId());
OrderState s = OrderState.NEW;
s = s.moveTo(OrderState.PARTIALLY_FILLED);
s = s.moveTo(OrderState.FILLED);
System.out.println("state " + s);
try {
s.moveTo(OrderState.CANCELLED);
} catch (IllegalStateException e) {
System.out.println("rejected " + e.getMessage());
}
}
}id 1, 2
state FILLED
rejected FILLED -> CANCELLEDThe singleton's next field is mutable and not thread-safe; an enum guarantees one instance, not safe access to its state. The transition table lives in one method, so adding a state forces an update to the exhaustive switch.
Example 7: declaring and reading annotations
An annotation type is declared with @interface. Its elements look like methods without parameters and may have defaults. Two meta-annotations (annotations on annotation types) control its use:
| Meta-annotation | Effect |
|---|---|
@Retention(SOURCE) |
discarded by the compiler (@Override) |
@Retention(CLASS) |
kept in the class file, not visible to reflection (the default) |
@Retention(RUNTIME) |
visible to reflection |
@Target({...}) |
where it may appear: TYPE, METHOD, FIELD, PARAMETER, RECORD_COMPONENT, and more |
@Inherited |
a class annotation is also reported on subclasses (classes only, not interfaces or methods) |
@Repeatable(Container.class) |
the annotation may appear more than once; the compiler wraps the copies in the container |
Reflection is reading a program's classes, methods, and fields at run time through Class objects; packages, modules, and reflection covers it in full. This example uses only the annotation methods.
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class AnnotationBasics {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
@interface Component { String value() default "unnamed"; }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Tags.class)
@interface Tag { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Tags { Tag[] value(); }
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
@interface BuildOnly {}
@Component("pricer") @Tag("fx") @Tag("rates") @BuildOnly
static class Pricer {}
static class FastPricer extends Pricer {}
public static void main(String[] args) {
Component c = Pricer.class.getAnnotation(Component.class);
System.out.println("component " + c.value());
System.out.println("inherited " + FastPricer.class.isAnnotationPresent(Component.class));
for (Tag t : Pricer.class.getAnnotationsByType(Tag.class)) {
System.out.println("tag " + t.value());
}
System.out.println("direct Tag " + Pricer.class.getAnnotation(Tag.class));
System.out.println("Tags container " + Pricer.class.isAnnotationPresent(Tags.class));
System.out.println("BuildOnly visible " + Pricer.class.isAnnotationPresent(BuildOnly.class));
System.out.println("FastPricer tags " + FastPricer.class.getAnnotationsByType(Tag.class).length);
}
}component pricer
inherited true
tag fx
tag rates
direct Tag null
Tags container true
BuildOnly visible false
FastPricer tags 0getAnnotation(Tag.class) returns null because two @Tag annotations are stored as one @Tags container; getAnnotationsByType looks through the container. @BuildOnly has CLASS retention, so reflection does not see it. Tag is not @Inherited, so the subclass reports no tags.
Example 8: a validator driven by annotations
Frameworks such as validation and serialization libraries read field annotations at run time. This example builds a small version: @NotBlank and @Range on record components, checked by a generic validate method. Annotations with target FIELD placed on a record component are propagated to the generated private field.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
public class AnnotationValidator {
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface NotBlank {}
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface Range { long min(); long max(); }
record NewOrder(@NotBlank String symbol, @Range(min = 1, max = 1_000_000) long quantity,
@Range(min = 1, max = 100_000) long priceCents) {}
static List<String> validate(Object target) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
for (Field f : target.getClass().getDeclaredFields()) {
f.setAccessible(true);
Object value = f.get(target);
if (f.isAnnotationPresent(NotBlank.class)
&& (value == null || value.toString().isBlank())) {
errors.add(f.getName() + " must not be blank");
}
Range r = f.getAnnotation(Range.class);
if (r != null) {
long v = (Long) value;
if (v < r.min() || v > r.max()) {
errors.add(f.getName() + " " + v + " not in [" + r.min() + ", " + r.max() + "]");
}
}
}
return errors;
}
public static void main(String[] args) throws IllegalAccessException {
System.out.println(validate(new NewOrder("AAPL", 100, 18_950)));
System.out.println(validate(new NewOrder(" ", 0, 18_950)));
System.out.println(validate(new NewOrder("MSFT", 5, 200_000)));
}
}[]
[symbol must not be blank, quantity 0 not in [1, 1000000]]
[priceCents 200000 not in [1, 100000]]getDeclaredFields returns a record's fields in component order, so the error list is stable here. For ordinary classes the order of getDeclaredFields is not specified, and production validators sort or index fields by name. Reflection costs time on every call; libraries read annotations once per class and cache the result.
An annotation processor is a compiler plugin (a class extending javax.annotation.processing.AbstractProcessor) that reads annotations with SOURCE or CLASS retention during compilation and generates new source files or reports errors. MapStruct, Dagger, and Immutables work this way. The public processor API cannot modify existing classes, only add files; Lombok changes classes by calling internal javac APIs instead. Since JDK 23, javac runs processors found on the class path only when asked, for example with -processor, --processor-path, or -proc:full.
Interview points
- Enum constants are singletons per class loader; compare with
==. - Never persist
ordinal(); it changes when constants are reordered or inserted. - A one-constant enum is a singleton that survives serialization and resists reflective construction.
- Constant-specific bodies make each constant an anonymous subclass; use
getDeclaringClass()for the enum type. EnumSetis onelongfor up to 64 constants (RegularEnumSet),JumboEnumSetabove;EnumMapis an ordinal-indexed array. Both iterate in declaration order.- An enum constructor cannot read the enum's own non-constant static fields; build lookup maps in a static block.
- Retention
CLASSis the default and is not visible to reflection; frameworks needRUNTIME. @Inheritedapplies only to class annotations inherited from superclasses.- Repeated annotations are stored in a container;
getAnnotationreturnsnullfor them andgetAnnotationsByTypefinds them. - Annotation processors generate new files at compile time; they cannot change existing classes through the standard API.
Summary
- Every enum extends
java.lang.Enum, isComparableby ordinal, and getsvalues()andvalueOf(String). - Enums can have fields, private constructors, methods, and per-constant bodies.
- A
switchexpression over all constants is exhaustive and needs nodefault;nullthrowsNullPointerException. EnumSetandEnumMapare the fastest and smallest set and map for enum keys.- Annotations are declared with
@interface, restricted by@Target, kept according to@Retention. getAnnotation,isAnnotationPresent, andgetAnnotationsByTypereadRUNTIMEannotations.
