Flashcards for topic Enums and Annotations
Implement a type-safe fromString
method for an enum with custom string representations that properly handles the case when an invalid string is provided.
public enum Operation { PLUS("+"), MINUS("-"), TIMES("*"), DIVIDE("/"); private final String symbol; Operation(String symbol) { this.symbol = symbol; } @Override public String toString() { return symbol; } // Map to efficiently convert strings back to enum constants private static final Map<String, Operation> stringToEnum = Stream.of(values()).collect( toMap(Object::toString, e -> e)); // Returns Operation for string, handles invalid inputs with Optional public static Optional<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); } } // Client code must handle the Optional: Operation.fromString("+").ifPresent(op -> System.out.println("Found operation: " + op));
Using Optional<Operation>
forces clients to consider the possibility that the string might not represent a valid operation.
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 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 is a cascaded collector sequence and how is it used to initialize a nested EnumMap?
A cascaded collector sequence chains multiple collectors to transform stream elements into a complex data structure. For initializing a nested EnumMap:
// Creating Map<Phase, Map<Phase, Transition>> private static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values()) // Stream of Transition values .collect( // First collector: Group by "from" phase groupingBy(t -> t.from, // Factory for outer map () -> new EnumMap<>(Phase.class), // Second collector: Create inner map toMap( t -> t.to, // Key mapper (destination phase) t -> t, // Value mapper (transition itself) (x, y) -> y, // Merge function (unused but required) () -> new EnumMap<>(Phase.class) // Factory for inner map ) ) );
Breakdown:
First collector (groupingBy
):
EnumMap<Phase, ...>
as the outer mapSecond collector (toMap
):
EnumMap<Phase, Transition>
as inner mapThe result is a type-safe nested map structure with the performance benefits of EnumMap, representing phase transitions as from → to → transition
.
What is the difference between EnumSet and EnumMap, and when should you use each?
EnumSet:
Set
interface for enum elementsEnumSet<DayOfWeek> weekend = EnumSet.of(SATURDAY, SUNDAY);
EnumMap:
Map
interface with enum keysEnumMap<DayOfWeek, Integer> hoursOpen = new EnumMap<>(DayOfWeek.class); hoursOpen.put(MONDAY, 9);
When to use each:
Neither should be used across different enum types - use one EnumSet/EnumMap per enum type.
What happens when you use isAnnotationPresent()
with a repeatable annotation type instead of its containing annotation type?
Using isAnnotationPresent()
with a repeatable annotation type will return false
for elements that have the repeatable annotation applied multiple times, leading to silently ignored annotations. This happens because:
// Correct way to detect both repeated and non-repeated annotations if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) { // Process the annotations }
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; } }
Explain how bounded type tokens work with annotation parameters, using the example of exception type checking.
Bounded type tokens with annotations:
// Annotation with bounded type token parameter public @interface ExceptionTest { Class<? extends Throwable> value(); // Bounded type token } // Usage @ExceptionTest(ArithmeticException.class) public static void testMethod() { // Method implementation } // Processing Class<? extends Throwable> expectedExcType = method.getAnnotation(ExceptionTest.class).value(); // Type checking with isInstance if (expectedExcType.isInstance(thrownException)) { // Exception matches expected type }
Key concepts:
Class<? extends Throwable>
restricts the type parameter to Throwable subtypesisInstance()
allows checking if an object is an instance of the class or a subclassThe bounded type token ensures that only exception types can be specified as the annotation value while maintaining type safety throughout the code.
When the @Override annotation is applied to a method that doesn't actually override a superclass/interface method, what specifically happens?
When @Override is applied to a method that doesn't actually override anything, the compiler generates an error message. For example:
@Override public boolean equals(Bigram b) { ... }
Will produce a compile-time error similar to:
error: method does not override or implement a method from a supertype
@Override public boolean equals(Bigram b) { ... }
^
This compiler behavior is extremely valuable because it:
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!
Showing 10 of 43 cards. Add this deck to your collection to see all cards.