Extensible Enums and Annotations
Source: Effective Java, 3rd Edition — Chapter 6 by Joshua Bloch (Items 38–41)
This lesson closes Chapter 6: emulate extensible enums with interfaces, prefer annotations over naming patterns, use @Override, and choose marker interfaces vs marker annotations.
Item 38: Emulate extensible enums with interfaces
Language enums cannot be extended — by design (confusing subtyping, no way to iterate base + extensions). For opcodes where clients add operations, use an interface + a standard enum that implements it:
public interface Operation { double apply(double x, double y); }
public enum BasicOperation implements Operation { PLUS(...), ... }
public enum ExtendedOperation implements Operation { EXP(...), REMAINDER(...); }
APIs take Operation, not BasicOperation. Clients pass ExtendedOperation instances or iterate via:
<T extends Enum<T> & Operation> void test(Class<T> opEnumType, ...)
// or
void test(Collection<? extends Operation> opSet, ...)
The Class<T> form enables EnumSet / EnumMap; the collection form is simpler and can mix implementation types. Shared logic can live in the interface as default methods (Item 20). Used in LinkOption implements CopyOption, OpenOption.
Item 39: Prefer annotations to naming patterns
Naming patterns (testSafetyOverride, encoding exception types in method names) fail silently on typos, attach to wrong elements, and cannot carry structured parameters.
Annotations attach metadata read by tools/frameworks. Example marker annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }
Meta-annotations: @Retention, @Target. Marker annotations have no elements; single-member annotations use value() (e.g. Class<? extends Throwable> value() for expected exceptions). Array parameters accept { A.class, B.class }; Java 8 @Repeatable wraps repeated annotations in a container type.
A toy runner uses reflection: Method.isAnnotationPresent(Test.class), invoke(null) for static tests. InvocationTargetException.getCause() holds the test failure. Annotations do not change semantics — they enable tools.
Annotations vs naming
1 / 2JUnit 3 testTsetFoo typo in method name
Pick an answer to see if you are right.
Item 40: Consistently use the Override annotation
Bigram with equals(Bigram b) overloads instead of overriding Object.equals(Object) — Set keeps 260 duplicates instead of 26. @Override on the broken signature makes the compiler error: method does not override.
Fix: @Override public boolean equals(Object o) with instanceof + cast (Item 10). Use @Override on every method you intend to override (including interface defaults you implement). Minor exception: concrete classes need not annotate overrides of abstract methods (compiler enforces anyway) — annotating anyway is fine.
Item 41: Use marker interfaces to define types
A marker interface has no methods — it marks a type (Serializable). Marker annotations (@Marker) are more flexible for frameworks but do not define a type — no compile-time parameter checking.
| Marker interface | Marker annotation | |
|---|---|---|
| Defines a type | yes | no |
| Compile-time parameter checks | yes | no |
| Applies to non-types | no | yes (methods, fields, …) |
| Fits annotation-heavy frameworks | awkward | natural |
If you might write void process(Serializable x), use a marker interface. If marking is only for tooling and never a parameter type, prefer an annotation. If @Target(TYPE) but you need a type, reconsider a marker interface (inverse of Item 22).
Marker interfaces
1 / 2void serialize(Serializable s) vs annotate with @Serializable
Pick an answer to see if you are right.
Items 38–41 map
| Item | Move |
|---|---|
| 38 | Interface + enum for extensible opcodes; APIs use the interface |
| 39 | Annotations over naming patterns; retention, target, reflection |
| 40 | @Override on every intended override — catches overload mistakes |
| 41 | Marker interface when you need a type; annotation for non-type marks |
Chapter 6 complete. Next: Lambdas and Method References (Chapter 7, Items 42–44).