Chapter 5: Enums and Annotations
effective-java enums annotations java best-practices
Book: Effective Java, 3rd Edition — Joshua Bloch
Status: 🟩 Complete
Difficulty: Medium
Items: 34-41 (8 items)
Time to complete: ~45 min
Overview
Java’s type system offers powerful tools for representing fixed sets of constants and for attaching structured metadata to program elements. Before Java 1.5, programmers used int constants and naming conventions (like APPLE_FUJI, APPLE_PIPPIN) — a fragile, namespace-polluting, and undebuggable approach. Enums and annotations replaced both patterns with first-class language features that are type-safe, readable, and extensible.
This chapter covers how to use enums (introduced in Java 1.5) correctly — including how to attach behavior to constants, avoid ordinal abuse, and use EnumSet/EnumMap for efficient collection operations. It also covers annotations as the modern replacement for brittle naming conventions, and the subtleties of marker interfaces vs. marker annotations.
These items are particularly relevant in enterprise Java codebases, where enums often carry business logic (state machines, pricing rules, operation types) and annotations are the backbone of frameworks like Spring and JPA.
Items
Item 34: Use Enums Instead of int Constants
The Problem
The int enum pattern was used before Java 1.5. It is type-unsafe, has no namespace, is fragile (ordinal values are baked into clients), and is completely opaque in debugging output.
// BAD: int enum pattern — do not use
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
// Compiler allows this — silently wrong!
double ratio = APPLE_FUJI / ORANGE_NAVEL; // compares across typesProblems:
- No type safety: an
ORANGEint can be passed where anAPPLEint is expected - No namespace: must use prefixes like
APPLE_to avoid collisions - Printed values are meaningless integers
- Iterating over the set of values requires manual bookkeeping
- Adding/reordering constants breaks clients that rely on ordinal values
The Solution
Use a Java enum. Enums are full classes — they are implicitly public static final instances of the enum class, which is itself a subclass of java.lang.Enum.
// GOOD: Basic enum
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
// Type safety enforced at compile time
// You cannot accidentally pass an Orange where an Apple is expectedEnums can carry data and behavior. This is their most powerful feature — each constant can behave differently.
// GOOD: Enum with data and method — Planet example (from the book)
public enum Planet {
MERCURY (3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER (1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE (1.024e+26, 2.477e7);
private final double mass; // In kilograms
private final double radius; // In meters
public static final double G = 6.67300E-11;
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
// Surface gravity in m/s²
public double surfaceGravity() {
return G * mass / (radius * radius);
}
// Weight on this planet given object mass in kg
public double surfaceWeight(double mass) {
return mass * surfaceGravity();
}
}
// Usage
double earthWeight = 75.0; // kg
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
System.out.printf("Your weight on %s is %6.2f%n", p, p.surfaceWeight(mass));
}Enums with constant-specific method implementations (using abstract methods or method overriding) allow each constant to define its own behavior cleanly — this is the strategy pattern baked into the language:
// GOOD: Operation enum with constant-specific behavior
public enum Operation {
PLUS("+") { @Override public double apply(double x, double y) { return x + y; } },
MINUS("-") { @Override public double apply(double x, double y) { return x - y; } },
TIMES("*") { @Override public double apply(double x, double y) { return x * y; } },
DIVIDE("/") {
@Override
public double apply(double x, double y) {
if (y == 0) throw new ArithmeticException("Division by zero");
return x / y;
}
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
public abstract double apply(double x, double y);
@Override
public String toString() { return symbol; }
// Reverse lookup by symbol
private static final Map<String, Operation> STRING_TO_ENUM =
Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(STRING_TO_ENUM.get(symbol));
}
}Why This Works
Enums are singleton instances — there is exactly one instance per constant, enforced by the JVM. This gives you:
- Identity comparison with
==(safe and correct) name(),ordinal(),values(),valueOf()for free- Serialization support (enum constants are guaranteed to deserialize to the same object)
- Thread safety on the constants themselves (construction happens in the static initializer)
When to Apply / When NOT to Apply
- Apply whenever you have a fixed, known-at-compile-time set of constants
- Apply when constants need associated data or behavior (use fields + constructor)
- Do NOT use if the set of values is not fixed at compile time (use a class with constants, or a database-driven approach)
- Do NOT expose the
ordinal()value in APIs (see Item 35)
Java 17 Update
Java 14+ switch expressions make enum-based dispatch cleaner and exhaustiveness-checked by the compiler:
// Java 14+: switch expression with enum — exhaustiveness is compile-time checked
public double apply(double x, double y) {
return switch (this) {
case PLUS -> x + y;
case MINUS -> x - y;
case TIMES -> x * y;
case DIVIDE -> x / y;
};
// Compiler error if you forget a case — no need for a default!
}Java 17 sealed classes and pattern matching for switch (preview, finalized in Java 21) complement enums by providing similar exhaustiveness guarantees for class hierarchies. See ch06-lambdas-and-streams for how lambdas and streams interact with enum iteration.
Item 35: Use Instance Fields Instead of Ordinals
The Problem
Every enum has an ordinal() method that returns the position of the constant in the declaration (0-indexed). It is extremely tempting to derive associated integer values from ordinals. This creates a maintenance time bomb.
// BAD: Using ordinal() to derive a value
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
// Derived from ordinal — extremely fragile!
public int numberOfMusicians() { return ordinal() + 1; }
}
// Problem 1: If you reorder constants, all values silently change
// Problem 2: You cannot have two constants with the same number of musicians
// (e.g., a double quartet is 8 musicians, same as OCTET — impossible to add)
// Problem 3: You cannot add a constant that skips a value (e.g., a 12-piece ensemble)The Solution
Store the associated value in an instance field. The ordinal is irrelevant; the field provides the meaning.
// GOOD: Instance field stores the meaningful value
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
// Now you can freely reorder, add, or duplicate counts without breaking anythingWhy This Works
The Javadoc for ordinal() explicitly states: “Most programmers will have no use for this method. It is designed for use by general-purpose enum-based data structures such as EnumSet and EnumMap.” Only use ordinal() if you are implementing a data structure like EnumSet/EnumMap — never for application logic.
When to Apply / When NOT to Apply
- Always use instance fields for any meaning you want to attach to enum constants
- The only legitimate use of
ordinal()is insideEnumSetandEnumMapimplementations - If you are ever tempted to write
SomeEnum.values()[someIndex], stop — use aMap-based lookup or redesign
Java 17 Update
No language changes directly address this. However, with record classes (Java 16+), you can build value types that pair naturally with enums as companion data:
// Companion record for an enum with complex state
public record EnsembleInfo(String name, int musicians, boolean isStandard) {}
public enum Ensemble {
SOLO(new EnsembleInfo("Solo", 1, true)),
DOUBLE_QUARTET(new EnsembleInfo("Double Quartet", 8, false));
private final EnsembleInfo info;
Ensemble(EnsembleInfo info) { this.info = info; }
public EnsembleInfo info() { return info; }
}Item 36: Use EnumSet Instead of Bit Fields
The Problem
Before enums, a common pattern for representing sets of constants was the bit field — assigning each constant a distinct power of 2, then combining them with bitwise OR.
// BAD: Bit field pattern
public class Text {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
// Pass a combination of STYLE_ constants
public void applyStyles(int styles) { ... }
}
// Usage — completely opaque
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
// Problems:
// - What does "3" mean when printed? You have to know the bit layout.
// - Adding a 33rd constant requires migrating from int to long
// - No type safety — any int can be passed
// - Iterating over the elements of a bit field is cumbersomeThe Solution
Use EnumSet — it is a high-performance Set implementation for enum types, internally implemented using bit vectors. You get type safety, expressiveness, and performance comparable to manual bit manipulation — with none of the fragility.
// GOOD: EnumSet
public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
// EnumSet<Style> instead of a bit field integer
public void applyStyles(Set<Style> styles) { ... }
// Note: Accept Set<Style> (interface), not EnumSet<Style> — more flexible
}
// Usage — readable and type-safe
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
// Other useful EnumSet operations
EnumSet<Style> allStyles = EnumSet.allOf(Style.class);
EnumSet<Style> noStyles = EnumSet.noneOf(Style.class);
EnumSet<Style> someStyles = EnumSet.copyOf(someCollection);
EnumSet<Style> range = EnumSet.range(Style.BOLD, Style.UNDERLINE); // contiguous rangeWhy This Works
EnumSet internally uses a single long bit vector for enums with up to 64 constants (which covers almost every real-world use case). Operations like add, contains, remove, and set algebra (or, and, xor) are all implemented as bitwise operations — exactly as fast as the manual bit-field approach, but with the full Set interface.
When to Apply / When NOT to Apply
- Whenever you have a “flags” or “options” parameter representing a combination of enum constants
- Accept
Set<MyEnum>in APIs (notEnumSet) to allow callers to pass anySetimplementation - The only downside:
EnumSetcannot be made immutable viaCollections.unmodifiableSetwithout a wrapper (thoughSet.copyOf()in Java 10+ creates an unmodifiable copy efficiently)
Java 17 Update
Set.copyOf(enumSet) (Java 10+) produces an unmodifiable copy. Note that Set.of(...) does NOT return an EnumSet, so for performance-critical code that must remain an EnumSet, stick with EnumSet directly. No fundamental changes in Java 17.
// Creating an immutable view
Set<Style> immutableStyles = Collections.unmodifiableSet(EnumSet.of(Style.BOLD, Style.ITALIC));
// Or for a true immutable copy (not an EnumSet but unmodifiable):
Set<Style> copy = Set.copyOf(EnumSet.of(Style.BOLD, Style.ITALIC));Item 37: Use EnumMap Instead of Ordinal Indexing
The Problem
A common mistake is to use ordinal() to index into an array (or ArrayList) to group objects by their enum type. This is another form of the ordinal-abuse antipattern from Item 35.
// BAD: Ordinal indexing to group plants by life cycle
public class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
}
// Grouping by life cycle using ordinal indexing — DANGEROUS
Set<Plant>[] plantsByLifeCycle =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length]; // unchecked cast
for (int i = 0; i < plantsByLifeCycle.length; i++)
plantsByLifeCycle[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
// Problems:
// - Unchecked cast (compiler warning)
// - Must use integer index, with no safety if ordinal changes
// - int[] index labels are not documented — "what does index 2 mean?"The Solution
Use EnumMap — a high-performance Map implementation keyed on enum types, internally backed by an array indexed by ordinal (so the performance is identical), but with all the type safety and readability of a proper Map.
// GOOD: EnumMap for grouping
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values())
plantsByLifeCycle.put(lc, new HashSet<>());
for (Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
// Output: {ANNUAL=[...], PERENNIAL=[...], BIENNIAL=[...]}
// Keys are self-documenting — no integer index neededFor a more streams-based approach:
// GOOD: Stream + EnumMap via groupingBy
Map<Plant.LifeCycle, List<Plant>> plantsByLifeCycle =
Arrays.stream(garden)
.collect(Collectors.groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(Plant.LifeCycle.class),
Collectors.toList()));For two-dimensional relationships (e.g., phase transitions), use nested EnumMap instead of a 2D array:
// GOOD: Two-level EnumMap for phase transitions
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// Build the lookup table once at class initialization
private static final Map<Phase, Map<Phase, Transition>> m =
Stream.of(values()).collect(
Collectors.groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
Collectors.toMap(t -> t.to,
t -> t,
(x, y) -> y,
() -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
}
// Adding a new phase (e.g., PLASMA) requires only adding a constant and transitions
// The map builds itself — no index arithmetic, no magic numbersWhy This Works
EnumMap internally uses an array indexed by the enum’s ordinal — so lookup is O(1) and as fast as array indexing — but the ordinal management is encapsulated inside EnumMap. Callers use the enum constant as the key, which is both type-safe and self-documenting.
When to Apply / When NOT to Apply
- Whenever you need a map keyed on enum constants — always prefer
EnumMapoverHashMap - The
groupingBy+EnumMapsupplier pattern is the canonical way to build these from streams - Do NOT use
ordinal()to index arrays/lists directly anywhere in application code
Java 17 Update
No fundamental changes. The groupingBy + EnumMap collector pattern remains the gold standard. Java 16’s Stream.toList() can simplify the downstream collector in some cases, but EnumMap usage itself is unchanged.
Item 38: Emulate Extensible Enums with Interfaces
The Problem
Java enums cannot be extended — you cannot have one enum inherit from another. This is intentional: extending an enum would allow instances of the subtype to be elements of the base type, which breaks the contract that the set of enum constants is fixed and exhaustive.
But sometimes you want to let users extend a set of operations — for example, a library defines basic arithmetic operations, and clients want to add their own (like exponentiation).
// BAD: Trying to share behavior via a base class or inheritance
// This is not possible — enums cannot extend other enums
public enum ExtendedOperation extends BasicOperation { ... } // COMPILE ERRORThe Solution
Define an interface that the enum implements. Clients can then define their own enums that implement the same interface, achieving extensibility.
// GOOD: Interface-based extensible enum
// Step 1: Define the interface
public interface Operation {
double apply(double x, double y);
}
// Step 2: Basic implementation
public enum BasicOperation implements Operation {
PLUS("+") { @Override public double apply(double x, double y) { return x + y; } },
MINUS("-") { @Override public double apply(double x, double y) { return x - y; } },
TIMES("*") { @Override public double apply(double x, double y) { return x * y; } },
DIVIDE("/") { @Override public double apply(double x, double y) { return x / y; } };
private final String symbol;
BasicOperation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
}
// Step 3: Client extension — a new enum implementing the same interface
public enum ExtendedOperation implements Operation {
EXP("^") { @Override public double apply(double x, double y) { return Math.pow(x, y); } },
REMAINDER("%") { @Override public double apply(double x, double y) { return x % y; } };
private final String symbol;
ExtendedOperation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
}
// Usage with bounded type token — accepts any enum implementing Operation
public static <T extends Enum<T> & Operation> void test(Class<T> opType, double x, double y) {
for (Operation op : opType.getEnumConstants())
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
test(ExtendedOperation.class, 4, 2); // Works with either enum
// Alternative: pass a Collection<? extends Operation>
public static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet)
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
test(Arrays.asList(ExtendedOperation.values()), 4, 2);Why This Works
The interface is the abstraction point. Code that works with Operation is oblivious to which specific enum implements it — it can accept BasicOperation, ExtendedOperation, or any future enum. The compiler enforces type safety at each enum’s constant-specific method body.
When to Apply / When NOT to Apply
- Apply when you’re designing a library API where clients should be able to add their own constants to an operation set
- Do NOT apply when you need the exhaustive-case-checking guarantee of a single closed enum — the interface approach sacrifices exhaustiveness
- Common in framework design (e.g., a plugin system where operation types are plugged in)
Java 17 Update — Sealed Interfaces
Java 17’s sealed interfaces create an interesting interaction. A sealed interface restricts which classes/enums can implement it, giving you the best of both worlds: extensibility within a controlled set.
// Java 17: Sealed interface + multiple implementing enums
public sealed interface Operation permits BasicOperation, ExtendedOperation {
double apply(double x, double y);
}
// Now the compiler knows the complete set of implementors at compile time
// Switch expressions on Operation can be exhaustiveness-checked (Java 21 finalized this)
public double compute(Operation op, double x, double y) {
return switch (op) {
case BasicOperation.PLUS -> x + y;
case BasicOperation.MINUS -> x - y;
case BasicOperation.TIMES -> x * y;
case BasicOperation.DIVIDE -> x / y;
case ExtendedOperation.EXP -> Math.pow(x, y);
case ExtendedOperation.REMAINDER -> x % y;
// No default needed — compiler knows all cases
};
}This is a powerful pattern in Java 17-21: sealed interfaces + enum pattern matching gives exhaustiveness checking across a family of enums. This doesn’t contradict Item 38 — it refines it.
Item 39: Prefer Annotations to Naming Patterns
The Problem
Before annotations (pre-Java 1.5), frameworks used naming patterns to signal special treatment. JUnit 3 required test method names to start with test. This approach is error-prone:
// BAD: Naming pattern to signal framework behavior — JUnit 3 style
public class MyTests {
// TYPO: "tsetSomething" — JUnit silently ignores this! No error.
public void tsetSomethingImportant() { ... }
// If you put it in the wrong class, also silently ignored
}
// Problems:
// - Typos are silently swallowed — tests never run
// - No way to associate parameters with the marker
// - No compile-time checking
// - No IDE support for "show all tests"The Solution
Use annotations. Annotations provide compile-time checking (with appropriate processors), proper IDE integration, and can carry parameters.
// GOOD: Custom annotation definition (mimicking JUnit 4's @Test)
// Marker annotation — takes no parameters
@Retention(RetentionPolicy.RUNTIME) // Annotation survives to runtime
@Target(ElementType.METHOD) // Only applicable to methods
public @interface Test { }
// Annotation with a parameter
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); // Single required parameter
}
// Annotation with an array parameter (repeatable alternative)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable>[] value(); // Array allows multiple exception types
}
// Repeatable annotation (cleaner in Java 8+)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}Usage in a test class:
// GOOD: Using custom annotations
public class Sample {
@Test
public void testNormalMethod() { ... }
@ExceptionTest(ArithmeticException.class)
public void testDivideByZero() {
int i = 0;
i = i / i; // Should throw ArithmeticException
}
// Repeatable: expect either exception
@ExceptionTest(ArithmeticException.class)
@ExceptionTest(ArrayIndexOutOfBoundsException.class)
public void testMultipleExceptions() { ... }
}Processing annotations via reflection:
// Framework/runner code — processes annotations
public static void runTests(Class<?> testClass) throws Exception {
int passed = 0, failed = 0;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedEx) {
failed++;
System.out.println(m + " FAILED: " + wrappedEx.getCause());
} catch (Exception ex) {
System.out.println("Invalid @Test: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n", passed, failed);
}Why This Works
Annotations are a first-class language feature with defined retention and target semantics. They cannot be misspelled without the compiler catching it (for well-defined annotation types). They can carry structured data (parameters). They integrate with IDEs, build tools, and annotation processors.
When to Apply / When NOT to Apply
- Always prefer annotations when designing frameworks, test runners, or any system that needs to mark or configure program elements
- The naming convention approach is only acceptable in the rare case where you cannot control the runtime (e.g., certain legacy systems) — even then, a custom annotation processor is almost always better
- Do not define your own annotation if a standard one exists (
@Override,@Deprecated,@SuppressWarnings,@FunctionalInterface, etc.)
Java 17 Update
No fundamental changes to the annotation system. @Repeatable (Java 8) remains the cleanest way to allow multiple uses of the same annotation. Annotation processing via javax.annotation.processing is stable. The major framework ecosystems (Spring, Quarkus, Micronaut) have doubled down on annotations — Quarkus and Micronaut move annotation processing to build time for performance.
Item 40: Consistently Use the Override Annotation
The Problem
Failing to use @Override when you intend to override a superclass method is a classic, silent bug in Java. The compiler cannot distinguish between overriding and overloading without your intent.
// BAD: Missing @Override — this OVERLOADS, not overrides
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
// WRONG: equals(Bigram) overloads Object.equals(Object), does NOT override it!
// This method is never called when using HashSet or HashMap
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++)
for (char ch = 'a'; ch <= 'z'; ch++)
s.add(new Bigram(ch, ch));
System.out.println(s.size()); // Prints 520, not 26!
}
}The Solution
Add @Override to every method you intend to override. The compiler will immediately flag the bug:
// GOOD: @Override catches the bug at compile time
@Override
public boolean equals(Bigram b) { ... }
// ERROR: Method does not override or implement a method from a supertype
// — you are forced to fix the signature to: public boolean equals(Object o)
// CORRECT fix:
@Override
public boolean equals(Object o) {
if (!(o instanceof Bigram)) return false;
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
@Override
public int hashCode() {
return 31 * first + second;
}The rule: use @Override on every method declaration that you believe overrides a superclass declaration. The only exception is abstract methods in abstract classes — you don’t need @Override there (though it’s still fine to include it).
// @Override on interface method implementations — strongly recommended
public class MyList<E> extends AbstractList<E> {
@Override
public E get(int index) { ... } // Overrides AbstractList.get()
@Override
public int size() { ... } // Overrides AbstractCollection.size()
}Why This Works
@Override is checked by the compiler. If you annotate a method with @Override and it does not actually override a supertype method, you get a compile-time error — the exact kind of early feedback you want. No annotation = no check = silent bugs at runtime.
When to Apply / When NOT to Apply
- Apply on every method you intend to override (including interface default method overrides)
- Apply on interface method implementations in classes (some style guides make this optional but Bloch recommends it)
- Not applicable to constructors (constructors don’t override)
- Not applicable to methods that are not intended to override (i.e., new methods in a subclass)
Java 17 Update
@Override works correctly with default interface methods and has since Java 8. With records (Java 16+), you can @Override methods like equals, hashCode, and toString that records auto-generate:
// Java 16+: Overriding record-generated methods
public record Point(int x, int y) {
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}With sealed classes (Java 17), every abstract method in the sealed parent must be implemented in each permitted subclass — @Override remains important to ensure you’re implementing rather than overloading.
Item 41: Use Marker Interfaces to Define Types
The Problem
A marker interface is an interface with no methods — it just “marks” a class as having some property. Serializable and Cloneable are the canonical examples. A marker annotation (like @Deprecated) serves a similar purpose. Bloch addresses when to use which.
The mistake is defaulting to marker annotations for everything and losing the type-safety that marker interfaces provide.
// BAD: Using a marker annotation when a marker interface would be better
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Persistent { }
// A method that accepts only Persistent objects — but the annotation provides no type check
public void save(@Persistent Object obj) { ... }
// This compiles fine even if @Persistent is not on obj's class:
save(new Object()); // No compile error — the annotation check happens at runtimeThe Solution
Use a marker interface when you want to define a type that can be used at compile time. If the marker is only meaningful at a specific point where you can write a type constraint, use an interface.
// GOOD: Marker interface — provides compile-time type safety
public interface Persistent { }
// Now the method signature enforces the constraint at compile time
public void save(Persistent obj) { ... }
// This WON'T compile — Object doesn't implement Persistent:
save(new Object()); // COMPILE ERRORThe Serializable interface in Java uses this pattern — ObjectOutputStream.writeObject() accepts only Object, not Serializable, which Bloch considers a mistake; it should have accepted Serializable to catch errors at compile time.
Marker Interface vs. Marker Annotation — Decision Framework
| Criterion | Use Marker Interface | Use Marker Annotation |
|---|---|---|
| Target is class/interface only | Yes | Either |
| Target includes method, field, param | No | Yes |
| Need compile-time type checking | Yes | No |
| Will be used in class literals (as type parameter bounds) | Yes | No |
| Part of a framework using annotation processing | No | Yes |
| Need to add information (parameters) in the future | Possibly (via methods with defaults) | Yes (easy to add params) |
// Summary of when to use each:
// Use marker INTERFACE when:
// 1. You want to define a type (like Serializable)
// 2. The marker applies only to classes/interfaces
// 3. You want compile-time type safety in method parameters
// Use marker ANNOTATION when:
// 1. The marker must apply to non-class elements (fields, methods, packages)
// 2. You're working in a framework that's already annotation-based
// 3. You want to add parameters to the marker later (easy with annotations)
// A hybrid: use both!
@HasMarkup // annotation for framework scanning
public interface Renderable { } // interface for type safetyWhy This Works
Interfaces define types. Annotations do not — they are metadata. When you want the compiler to enforce that only “marked” objects are passed to certain methods, an interface is the right tool. When you want to attach information to program elements for a framework to consume at runtime (via reflection) or compile time (via annotation processors), annotations are the right tool.
When to Apply / When NOT to Apply
- Use marker interfaces when the marker applies to class/interface declarations and you want type safety
- Use marker annotations when the marker needs to apply to elements other than classes, or when you are in an annotation-heavy framework context
- If you’re ever in doubt and writing a new framework, consider whether clients will need to write method signatures like
void process(MyMarker obj)— if yes, use an interface
Java 17 Update
Sealed interfaces (Java 17) are, in a sense, a more powerful marker interface — they mark a closed set of implementations while also providing compile-time exhaustiveness checking in switch expressions. They do not replace simple marker interfaces but offer an evolution:
// Java 17: Sealed marker interface — marks a closed set of serializable types
public sealed interface JsonSerializable
permits UserDto, ProductDto, OrderDto { }
// The compiler knows all types that implement JsonSerializable
public String toJson(JsonSerializable obj) {
return switch (obj) {
case UserDto u -> serializeUser(u);
case ProductDto p -> serializeProduct(p);
case OrderDto o -> serializeOrder(o);
// No default needed — sealed = exhaustive
};
}Interview Questions & Exercises
Q1: Why can’t you extend an enum in Java? How do you achieve extensibility?
Context: Asked in senior Java interviews when discussing design patterns, plugin architectures, or framework design.
Answer: Enums cannot be extended because the Java language specification prohibits extends on enum declarations — and for good reason. An enum’s contract is that its set of constants is fixed and known at compile time. If you could subtype an enum, instances of the subtype would be elements of the parent type’s domain, breaking the exhaustive-set guarantee (which compilers rely on for exhaustiveness in switch statements).
The correct extensibility approach (Item 38) is to define an interface that the enum implements:
public interface Op { double apply(double x, double y); }
public enum BasicOp implements Op { PLUS { public double apply(double x, double y) { return x+y; } } }
public enum ExtOp implements Op { EXP { public double apply(double x, double y) { return Math.pow(x,y); } } }Code that works with Op works with both enums. In Java 17+, a sealed interface allows you to restrict which enums can implement the interface, restoring partial exhaustiveness.
Follow-up: “What’s the tradeoff vs. using a sealed class hierarchy?” — Answer: enums give you values(), ordinal(), name(), valueOf(), and compatibility with EnumSet/EnumMap for free; class hierarchies do not.
Q2: A HashMap<SomeEnum, List- > is working correctly. Should you change it to EnumMap?
Context: Code review or performance optimization discussion.
Answer: Yes. EnumMap provides the same Map interface but is implemented internally as an array indexed by enum ordinal — all operations are O(1) with tiny constants (no hashing, no collision resolution, no resizing). It is also more memory-efficient than HashMap. There is no reason to use HashMap when all keys are from a single enum type.
// Before
Map<Status, List<Order>> byStatus = new HashMap<>();
// After — same interface, better performance
Map<Status, List<Order>> byStatus = new EnumMap<>(Status.class);The API is identical so refactoring is trivial. The only caveat: EnumMap maintains insertion order = enum declaration order, whereas HashMap has no defined order.
Follow-up: “What about EnumSet vs HashSet?” — Same principle applies. For sets of enum constants, always prefer EnumSet.
Q3: Explain the difference between @Retention(RUNTIME) and @Retention(CLASS).
Context: Asked when discussing annotation processing, frameworks (Spring, JPA), or reflection.
Answer:
RetentionPolicy.SOURCE: Annotation is discarded by the compiler. Used for compile-time-only tools (e.g.,@SuppressWarnings, Lombok).RetentionPolicy.CLASS(default): Annotation is written to the.classfile but not loaded by the JVM at runtime. Used for bytecode-level tools (ASM, bytecode weavers).RetentionPolicy.RUNTIME: Annotation is available at runtime via reflection. Used by frameworks that inspect annotations at runtime (Spring@Autowired, JUnit@Test, JPA@Entity).
Most custom annotations for runtime frameworks need RUNTIME. Forgetting this is a common bug — the annotation appears in source but method.isAnnotationPresent(MyAnnotation.class) returns false.
Follow-up: “Can annotation processors run at compile time only?” — Yes, annotation processors using javax.annotation.processing.Processor run at compile time and only need SOURCE or CLASS retention.
Q4: What bug does @Override catch? Show an example.
Context: Fundamental Java correctness question — asked at all levels.
Answer: The most common bug @Override catches is accidental overloading instead of overriding, usually due to a wrong parameter type. Classic example: trying to override Object.equals(Object o) but writing equals(MyType t):
// Without @Override — silently broken
public boolean equals(MyType other) { ... } // Overloads, doesn't override
// With @Override — compile error immediately
@Override
public boolean equals(MyType other) { ... }
// Error: Method does not override or implement a method from a supertypeThe fix is equals(Object o) with a cast inside. Without @Override, every instance compares by reference (the inherited Object.equals) and the bug only manifests at runtime when collections behave unexpectedly.
Follow-up: “Does @Override work on interface method implementations?” — Yes, since Java 6. It’s recommended even when implementing (not just overriding) interface methods.
Q5: When would you use a marker interface instead of a marker annotation?
Context: Design question in senior interviews about API design and type systems.
Answer: Use a marker interface when:
- The marker applies only to classes/interfaces (not methods, fields, parameters)
- You want the marker to define a type that can be used in method signatures for compile-time safety
- Clients need to write
void process(Serializable obj)or use the marker as a type bound
Use a marker annotation when:
- The marker must apply to non-class elements (methods, fields, packages)
- You’re in an annotation-heavy framework ecosystem
- You anticipate adding parameters to the marker later
Example: Serializable should have been used as the parameter type for ObjectOutputStream.writeObject(), which would have given compile-time checking. Instead, it accepts Object, and serialization errors surface at runtime.
Follow-up: “Can you use both?” — Yes. Define an interface for type safety AND an annotation for framework scanning/discovery.
Q6: How do you implement a “strategy pattern” using enums? What are the advantages?
Context: Asked in FAANG interviews on design patterns and OOP.
Answer: By using constant-specific method implementations — each enum constant overrides an abstract method declared in the enum body. This is the strategy pattern without a separate strategy class hierarchy:
public enum PaymentStrategy {
CREDIT_CARD {
@Override public double process(double amount) {
return amount * 1.02; // 2% fee
}
},
PAYPAL {
@Override public double process(double amount) {
return amount * 1.03; // 3% fee
}
},
CRYPTO {
@Override public double process(double amount) {
return amount; // no fee
}
};
public abstract double process(double amount);
}Advantages over a class-based strategy pattern:
- No need for a separate strategy interface + implementing classes
- Constants are singletons (no instantiation overhead)
switchdispatch is compile-time exhaustive with switch expressions (Java 14+)- Natural integration with
EnumSet/EnumMap values()for iterating all strategies
Follow-up: “What if the strategies need to vary at runtime?” — Then a class hierarchy is more appropriate; enum constants are fixed at compile time.
Q7: What is the “int enum pattern” and why is it still found in legacy code?
Context: Reading legacy code or interviewing at companies with old codebases.
Answer: The int enum pattern assigns named int constants to represent categorical values:
static final int STATUS_PENDING = 0;
static final int STATUS_ACTIVE = 1;
static final int STATUS_INACTIVE = 2;It predates Java 1.5 enums (2004). It’s still found in:
- Android development: Before Kotlin and modern Android,
@IntDefannotations were used to approximate type safety for int constants, partly for performance (enums have slightly higher memory overhead on Android) - Legacy enterprise codebases: Code written before Java 5 that was never refactored
- C-style API wrappers: JNI code mirroring C enums as ints
- Database column values: Integer constants sometimes stored in DB and mapped to constants in code
The correct modern replacement is always a Java enum, possibly combined with a database mapping layer (JPA @Enumerated(EnumType.STRING) to avoid ordinal fragility).
Follow-up: “Is there ever a performance reason to prefer int constants over enums?” — On standard JVM, no. On Android (especially old Dalvik), there was a small memory footprint argument, but even that is largely irrelevant on modern ART with 64-bit devices.
Q8: How does Java 17’s sealed interface feature affect Item 38 (extensible enums)?
Context: Advanced Java interview testing knowledge of modern Java features.
Answer: Item 38 recommends using interfaces to achieve extensibility across enum types. Java 17 sealed interfaces refine this by allowing you to restrict which enums can implement the interface, giving you controlled extensibility with compile-time exhaustiveness in pattern-matching switch expressions.
// Java 17: Sealed interface connecting a family of operation enums
public sealed interface Operation permits BasicOperation, ScientificOperation {
double apply(double x, double y);
}
// Java 21+ pattern matching with exhaustiveness
String describe(Operation op) {
return switch (op) {
case BasicOperation b -> "Basic: " + b.name();
case ScientificOperation s -> "Scientific: " + s.name();
// No default — compiler verifies exhaustiveness via sealed constraint
};
}Without sealed: you get open extensibility but lose exhaustiveness checking.
With sealed: you get controlled extensibility AND exhaustiveness checking.
The tradeoff: sealed interfaces require all implementors to be in the same module (or package without modules), so they don’t work for true third-party extension.
Key Takeaways
- Enums over int constants: Enums are type-safe, self-documenting, and can carry data and behavior. The int enum pattern is always inferior. Use switch expressions (Java 14+) for exhaustiveness-checked dispatch.
- Never use
ordinal()for application logic: Store meaningful data in instance fields.ordinal()exists only forEnumSet/EnumMapinternals. EnumSetfor bit flags: It is as fast as manual bit manipulation but type-safe, readable, and iterable. AcceptSet<MyEnum>in APIs.EnumMapover ordinal-indexed arrays: Same O(1) performance, but type-safe and self-documenting. UsegroupingBy+EnumMapsupplier in stream pipelines.- Extensible enums via interfaces: Define an interface the enum implements; clients write their own enums implementing the same interface. Java 17 sealed interfaces add controlled extensibility with exhaustiveness checking.
- Annotations over naming patterns: Annotations are compiler-checked, IDE-aware, and can carry structured data. Naming conventions are fragile and silent.
@Overridealways: Catches overloading-vs-overriding bugs at compile time. Use it on every method you intend to override, including interface implementations.- Marker interfaces define types; marker annotations attach metadata: Use a marker interface when you want compile-time type safety in method signatures; use a marker annotation when the marker targets non-class elements or you’re in an annotation-heavy framework.
- Enum + strategy pattern: Constant-specific method implementations give you a concise, singleton-safe strategy pattern with exhaustive switch dispatch.
- Java 14+ switch expressions: Paired with enums, they provide compile-time exhaustiveness checking — no more missing
defaultbugs for enum switches.
Last Updated: 2026-05-10