Skip to main content

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.

Quiz

Annotations vs naming

1 / 2

JUnit 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 interfaceMarker annotation
Defines a typeyesno
Compile-time parameter checksyesno
Applies to non-typesnoyes (methods, fields, …)
Fits annotation-heavy frameworksawkwardnatural

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).

Quiz

Marker interfaces

1 / 2

void serialize(Serializable s) vs annotate with @Serializable

Pick an answer to see if you are right.

Items 38–41 map​

ItemMove
38Interface + enum for extensible opcodes; APIs use the interface
39Annotations over naming patterns; retention, target, reflection
40@Override on every intended override — catches overload mistakes
41Marker interface when you need a type; annotation for non-type marks

Chapter 6 complete. Next: Lambdas and Method References (Chapter 7, Items 42–44).