enums and annotations
items 34–41: enums over ints, EnumSet, EnumMap, @Override.
- chapter
- chapter 16.05
- reading time
- 7 min read
a java enum is a full class with a fixed set of instances: it can have fields, methods and per-constant behavior, and the compiler checks every use. items 34 to 41: use enums instead of int constants, never lean on ordinal(), use EnumSet and EnumMap (they're as fast as bit tricks), and use annotations instead of naming conventions.
- item 34enums, not int constantsswitch expressions
- item 35never derive data from ordinal
- item 36EnumSet, not bit flags
- item 37EnumMap, not ordinal-indexed arrays
- item 38extensible enums go through an interfacesealed interfaces
- item 39annotations, not naming patterns
- item 40put @Override on every override
- item 41marker interfaces define types
enums
item 34 · enums, not int constants
static final int SMALL = 0 is just an int. the compiler can't stop you passing SMALL where a color was expected, it prints as 0, and recompiling is required when the value changes (it's inlined into callers). an enum is a type, prints its name, iterates with values(), and can carry data and behavior:
enum Tier {
FREE(0), TEAM(8), ENTERPRISE(21);
private final int perSeat;
Tier(int perSeat) { this.perSeat = perSeat; }
int monthly(int seats) {
return switch (this) { // exhaustive: add a tier, this stops compiling
case FREE -> 0;
case TEAM -> perSeat * seats;
case ENTERPRISE -> Math.max(500, perSeat * seats);
};
}
}
void main() {
for (Tier t : Tier.values()) IO.println(t + " " + t.monthly(10));
IO.println(Tier.valueOf("TEAM").ordinal());
}FREE 0 TEAM 80 ENTERPRISE 500 1
the book's alternative to switching on this is a constant-specific method body (each constant gets its own { ... } with its own monthly), which is still the right tool when each constant's behavior is big. and when several constants share a behavior, give them a nested "strategy" enum instead of repeating code. enums are also the cheapest correct singleton (item 3). the language side is in enums.
item 35 · never derive data from ordinal
ordinal() is the position in the declaration. it changes when someone reorders or inserts a constant, which nobody thinks of as a breaking change.
enum Priority { LOW, MEDIUM, HIGH } // MEDIUM was added last week
static int level(Priority p) { return p.ordinal() + 1; }
void main() {
IO.println(level(Priority.HIGH));
}enum collections
item 36 · EnumSet, not bit flags
the C habit: BOLD = 1, ITALIC = 2, UNDERLINE = 4, then style = BOLD | ITALIC. it's compact and fast, and it's an int with no idea what it means. EnumSet gives you the same bits with types and names on top.
same bits, same speed: contains is a mask, addAll is an OR. but the int accepts any int (pass it a port number, it compiles), prints as 3, and can't be iterated by name.
RegularEnumSet is one long for enums of up to 64 constants). the amber bit is the one you just flipped.enum Style { BOLD, ITALIC, UNDERLINE, STRIKE }
void main() {
var s = EnumSet.of(Style.BOLD, Style.ITALIC);
s.add(Style.STRIKE);
IO.println(s + " " + s.contains(Style.UNDERLINE));
IO.println(EnumSet.complementOf(s)); // every style not in s
IO.println(s.getClass().getSimpleName()); // one long underneath
}[BOLD, ITALIC, STRIKE] false [UNDERLINE] RegularEnumSet
the one gap: there's no immutable EnumSet. wrap it with Collections.unmodifiableSet, or copy it with Set.copyOf (which gives up the bit vector).
item 37 · EnumMap, not ordinal-indexed arrays
indexing an array by ordinal() works until the array and the enum disagree, and the array can't tell you which index meant what. EnumMap is an array indexed by ordinal internally, with all of that checked for you:
enum Stage { TODO, DOING, DONE }
record Task(String name, Stage stage) {}
void main() {
var tasks = List.of(new Task("tests", Stage.DONE), new Task("docs", Stage.TODO),
new Task("api", Stage.DOING), new Task("ci", Stage.DONE));
Map<Stage, List<String>> board = tasks.stream().collect(Collectors.groupingBy(
Task::stage, () -> new EnumMap<>(Stage.class), // keys come out in declaration order
Collectors.mapping(Task::name, Collectors.toList())));
IO.println(board);
IO.println(board.getClass().getSimpleName());
}{TODO=[docs], DOING=[api], DONE=[tests, ci]}
EnumMapfor "a value per pair of constants" (a transition table, say), nest them: Map<From, Map<To, Transition>>, built from a list of transitions so adding a constant can't misalign anything.
item 38 · extensible enums go through an interface
enums can't be extended, and usually that's right. when an API needs "the built-in operations plus whatever users add", define an interface, have your enum implement it, and accept the interface:
interface Unit { double toMeters(double v); }
enum Metric implements Unit {
MM { public double toMeters(double v) { return v / 1000; } },
KM { public double toMeters(double v) { return v * 1000; } };
}
record Furlong() implements Unit { // added by a user, no enum needed
public double toMeters(double v) { return v * 201.168; }
}annotations
item 39 · annotations, not naming patterns
old test frameworks found tests by name: a method starting with test was a test. misspell it (tsetPrice) and it silently never runs. an annotation is checked by the compiler, can carry parameters, and is found by reflection. here's a tiny test runner in about 30 lines:
import java.lang.annotation.*;
import java.lang.reflect.*;
@Retention(RetentionPolicy.RUNTIME) // keep it in the class file, visible to reflection
@Target(ElementType.METHOD) // only allowed on methods
@interface Check {}
class Pricing {
static int price(int seats) { return seats * 9; }
}
class PriceChecks {
@Check static void freeIsFree() { if (Pricing.price(0) != 0) throw new AssertionError("free costs money"); }
@Check static void bulkDiscount() { if (Pricing.price(100) >= 900) throw new AssertionError("no discount"); }
static void helper() { throw new IllegalStateException("not a check, never runs"); }
}
void main() throws Exception {
Method[] ms = PriceChecks.class.getDeclaredMethods();
Arrays.sort(ms, Comparator.comparing(Method::getName));
for (Method m : ms) {
if (!m.isAnnotationPresent(Check.class)) continue;
try {
m.invoke(null);
IO.println("pass " + m.getName());
} catch (InvocationTargetException e) {
IO.println("FAIL " + m.getName() + ": " + e.getCause().getMessage());
}
}
}FAIL bulkDiscount: no discount pass freeIsFree
this one needs a real JVM (reflection), so it doesn't run in the browser. copy it into java Main.java to try it.
that's JUnit's core idea in miniature. more on retention, targets and repeatable annotations in annotations.
item 40 · put @Override on every override
without it, a typo or a wrong parameter type turns your "override" into an overload, and nothing tells you:
class Tag {
final String name;
Tag(String name) { this.name = name; }
public boolean equals(Tag other) { return name.equals(other.name); }
@Override public int hashCode() { return name.hashCode(); }
}
void main() {
var tags = new HashSet<Tag>();
tags.add(new Tag("java"));
tags.add(new Tag("java"));
IO.println(tags.size());
}the one place you can skip it: implementing an abstract method in a concrete class, where the compiler already complains if you get it wrong. using it there too is harmless.
item 41 · marker interfaces define types
a marker interface has no methods (Serializable, Cloneable, RandomAccess). a marker annotation does the same job for tools. the difference: an interface is a type, so the compiler can check it in signatures (a method taking Serializable rejects non-serializable arguments at compile time). use an interface when you'd ever want to write a method that only accepts marked objects; use an annotation when you're marking methods or fields, or when a framework already speaks annotations.
- an enum is a class with fixed instances: fields, methods, per-constant behavior, type safety.
ordinal()is a position, not data. store numbers in fields.- EnumSet is a bit vector with types. EnumMap is an ordinal-indexed array with types.
- to let users add "enum constants", have the enum implement an interface.
- annotations beat naming conventions: checked, typed, found by reflection.
@Overrideon every override turns a silent overload into a compile error.