Flashcards for topic Enums and Annotations
How can you create an enum with both constant-specific behavior and data, and what constraints must you follow when implementing the constructor?
To create an enum with both constant-specific behavior and data:
public enum Operation { PLUS("+") { public double apply(double x, double y) { return x + y; } }, MINUS("-") { public double apply(double x, double y) { return x - y; } }, TIMES("*") { public double apply(double x, double y) { return x * y; } }, DIVIDE("/") { public double apply(double x, double y) { return x / y; } }; private final String symbol; // Constructor must come after all enum constants Operation(String symbol) { this.symbol = symbol; } // Common methods @Override public String toString() { return symbol; } // Abstract method enforces implementation for each constant public abstract double apply(double x, double y); }
Constructor constraints:
Key insight: Enums are "instance-controlled" classes that export exactly one instance for each constant via public static final fields.
What happens when you override toString()
in an enum type, and what additional methods should you consider implementing to provide bidirectional conversion between strings and enum constants?
When you override toString()
in an enum, you override the default string representation (the constant name) with a custom format.
Consequences:
valueOf(String)
method still expects the exact constant name, not your custom stringSolution: Implement a fromString
method:
public enum Operation { PLUS("+"), MINUS("-"), TIMES("*"), DIVIDE("/"); private final String symbol; Operation(String symbol) { this.symbol = symbol; } // Custom string representation @Override public String toString() { return symbol; } // Internal lookup map for reverse conversion private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect( toMap(Object::toString, e -> e)); // Custom string-to-enum conversion method public static Optional<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); } }
Key implementation details:
Optional<EnumType>
to handle invalid input stringsThis pattern maintains bidirectional conversion even with custom string formats.
What are the key differences in behavior between using EnumMap vs. stream-based collectors when grouping enum values?
Key behavioral differences:
Empty categories:
Map contents:
Implementation:
// EnumMap version - always contains all enum constants Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class); for (Plant.LifeCycle lc : Plant.LifeCycle.values()) plantsByLifeCycle.put(lc, new HashSet<>()); // Stream version - only contains enum constants with values Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = Arrays.stream(garden) .collect(groupingBy(p -> p.lifeCycle, () -> new EnumMap<>(LifeCycle.class), toSet()));
This difference matters when you need to process all enum constants, even those with no associated values.
What are the two approaches for using an "extensible enum" in client code, and what are the differences between them?
Approach 1: Using bounded type tokens with Class objects
// Client code public static void main(String[] args) { double x = 4.0, y = 2.0; test(ExtendedOperation.class, x, y); } // Bounded type parameter ensures Class represents both enum and Operation private static <T extends Enum<T> & Operation> void test( Class<T> opEnumType, double x, double y) { for (Operation op : opEnumType.getEnumConstants()) System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); }
Approach 2: Using bounded wildcard types with Collection
// Client code public static void main(String[] args) { double x = 4.0, y = 2.0; test(Arrays.asList(ExtendedOperation.values()), x, y); } // Using wildcard type for the collection private static void test(Collection<? extends Operation> operations, double x, double y) { for (Operation op : operations) System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y)); }
Differences:
What are three major disadvantages of using naming patterns (like the "test" prefix in JUnit 3) compared to annotations?
Disadvantages of naming patterns:
Silent failures from typographical errors
tsetSafetyOverride
instead of testSafetyOverride
No enforcement of appropriate usage locations
TestSafetyMechanisms
hoping JUnit would run all its methods@Target
metadata restricts where annotations can be usedNo way to associate parameter values with elements
testNullPointerException_safeDivide
@Test(expectedExceptions = NullPointerException.class)
These disadvantages make naming patterns error-prone and less powerful than annotations, which provide compile-time checking and richer metadata capabilities.
What happens when a method with parameters is annotated with @Test
in the example framework, and how could this be prevented?
When a parameterized method is annotated with @Test
:
Invalid @Test: public void SampleClass.parameterizedMethod(String s)
Prevention approaches:
Documentation comments (weak)
/** * Use only on parameterless static methods. */
Annotation processor (strong)
// Create a compile-time validator @SupportedAnnotationTypes("Test") public class TestAnnotationProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) { if (element.getKind() != ElementKind.METHOD) { processingEnv.getMessager().printError("@Test only allowed on methods", element); return true; } ExecutableElement method = (ExecutableElement) element; if (!method.getParameters().isEmpty()) { processingEnv.getMessager().printError("@Test methods must have no parameters", method); } if (!method.getModifiers().contains(Modifier.STATIC)) { processingEnv.getMessager().printError("@Test methods must be static", method); } } return true; } }
How would you handle the common antipattern of overloading equals()
instead of overriding it, and how can annotations help?
The equals()
overloading antipattern:
// WRONG: This overloads equals() instead of overriding it public boolean equals(Bigram b) { // Parameter should be Object return b.first == first && b.second == second; }
Solution using @Override
:
// CORRECT: Properly overrides Object.equals() @Override public boolean equals(Object o) { if (!(o instanceof Bigram)) return false; Bigram b = (Bigram) o; return b.first == first && b.second == second; }
Benefits of @Override
:
Common symptom of the bug: Collections behave incorrectly with your objects (sets don't eliminate duplicates, maps can't find elements).
When implementing the equals() method in a class, what are the exact issues that the @Override annotation helps catch that would otherwise manifest as subtle runtime bugs?
The @Override annotation for equals() catches several subtle issues:
Parameter Type Problems:
equals(MyClass o)
instead of equals(Object o)
Method Coexistence Bugs:
Signature Deviations:
Inheritance Chain Issues:
These issues typically cause difficult-to-debug equality problems in collections, serialization, and comparison operations.
Contrast the compile-time benefits of marker interfaces with the runtime properties of marker annotations. Provide an example where using a marker interface would prevent a bug that a marker annotation would not catch until runtime.
Compile-time Benefits of Marker Interfaces vs. Runtime Properties of Marker Annotations:
Marker Interfaces:
Marker Annotations:
Example Where Marker Interface Prevents a Bug:
// With marker interface public interface Processable { /* marker interface */ } public class Document implements Processable { /* ... */ } public class Image implements Processable { /* ... */ } public class Audio { /* doesn't implement Processable */ } // Method requires Processable objects public void process(Processable item) { /* ... */ } // COMPILE ERROR: Audio doesn't implement Processable process(new Audio()); // Caught at compile time! // With marker annotation instead @interface Processable { } @Processable class Document { /* ... */ } @Processable class Image { /* ... */ } class Audio { /* not annotated */ } // Method must check annotation at runtime public void process(Object item) { if (!item.getClass().isAnnotationPresent(Processable.class)) { throw new IllegalArgumentException("Not processable"); // Runtime error! } // ... } // No compile error, fails at runtime process(new Audio()); // Runtime exception!
When using marker interfaces that extend other interfaces, what pattern enables creating a "restricted marker interface"? How does this provide greater type safety than marker annotations?
Restricted Marker Interface Pattern:
A restricted marker interface is one that:
Implementation Pattern:
// Target interface public interface Collection<E> { ... } // Restricted marker interface public interface Set<E> extends Collection<E> { // No additional methods required // Implicitly restricts "Set-ness" to Collections only }
Type Safety Benefits:
Compile-time Restriction:
Hierarchy Enforcement:
Improved Method Signatures:
// With restricted marker interface: void processSet(Set<?> set) { ... } // Only accepts Sets // With marker annotation (weaker): void processSet(Collection<?> collection) { if (!collection.getClass().isAnnotationPresent(SetAnnotation.class)) { throw new IllegalArgumentException("Not a set"); } // ... }
This pattern creates stronger guarantees than annotations because the type system itself enforces the restriction, while annotations require explicit runtime checking code.
Showing 10 of 43 cards. Add this deck to your collection to see all cards.