Enums as real classes (and why they can't extend classes), iterating enums, the diamond operator, type inference, type erasure and why generic arrays are forbidden, pass-by-value explained with references, imports vs static imports, classes and objects in a real system, and this in static contexts.
Published September 25, 2026
These questions probe how well you understand what the compiler does for you: generics are erased, enums are generated classes, imports are compile-time only. Explaining the mechanism is what separates an intermediate answer from a fresher one.
Short answer: An enum is a type with a fixed, type-safe set of instances. Under the hood it's a final class extending java.lang.Enum, with one public static final instance per constant. Enums replace "magic" int or String constants, so invalid values become impossible and switch statements can be checked for exhaustiveness.
public enum OrderStatus {
PLACED { @Override boolean canCancel() { return true; } },
SHIPPED { @Override boolean canCancel() { return false; } },
DELIVERED { @Override boolean canCancel() { return false; } };
abstract boolean canCancel(); // behaviour per constant: no switch needed
}
Key points to cover:
EnumMap and EnumSet for very fast, compact enum-keyed collections.Short answer: No. Every enum already implicitly extends java.lang.Enum, and Java classes have only one superclass. Enums can implement interfaces, which is how you share behaviour, or make them pluggable.
interface PricingRule { BigDecimal apply(BigDecimal amount); }
enum Discount implements PricingRule {
NONE { public BigDecimal apply(BigDecimal a) { return a; } },
FESTIVE { public BigDecimal apply(BigDecimal a) { return a.multiply(new BigDecimal("0.90")); } }
}
Key points to cover:
final. The constant-specific bodies are anonymous subclasses, created by the compiler.Short answer: The answer doesn't change: no, because java.lang.Enum is already its superclass. When interviewers repeat the question, they're usually checking the follow-up: "so how would you share code between enums?" Use a common interface with default methods, or delegate to a helper class that each enum holds as a field.
Short answer: Call the compiler-generated static values() method. It returns a new array of the constants, in declaration order.
for (OrderStatus s : OrderStatus.values()) {
System.out.println(s.ordinal() + " " + s.name());
}
EnumSet.allOf(OrderStatus.class).forEach(System.out::println); // no array copy
Arrays.stream(OrderStatus.values()).filter(OrderStatus::canCancel).toList();
Key points to cover:
values() clones the array on every call. In hot paths, cache it (private static final OrderStatus[] ALL = values();), or use EnumSet.allOf.Short answer: values() with a for-each loop, EnumSet.allOf(...), or a stream over values(). To look a constant up by name, use Enum.valueOf(Type.class, "NAME") or Type.valueOf("NAME"). It throws IllegalArgumentException for an unknown name, so validate external input before calling it.
Short answer: <> (Java 7) lets the compiler infer the generic type arguments of a constructor call from the context, so you don't repeat them: Map<String, List<Order>> byCustomer = new HashMap<>();.
Key points to cover:
new ArrayList() (no <>) is a raw type, and loses type safety, with compiler warnings.Short answer: The compiler deduces the type arguments of generic methods and constructors from the arguments and the target type, so you rarely need to write them explicitly.
List<String> names = Collections.emptyList(); // T inferred as String from the target type
var map = Map.of("a", 1, "b", 2); // Map<String, Integer> inferred from the arguments
List<Integer> ids = Stream.of(3, 1, 2).sorted().toList();
Collections.<String>emptyList(); // explicit type witness: rarely needed
Key points to cover:
Learn it in depth → Generics
Short answer: Generic type information exists only at compile time. The compiler checks the types, inserts the necessary casts, then erases the type parameters. List<String> and List<Integer> both become the raw List in bytecode, and T becomes its bound (Object, or Comparable for <T extends Comparable<T>>). This kept generics backward-compatible with pre-Java 5 code.
Consequences worth naming:
new T(), no T.class, and no instanceof List<String>.m(List<String>) and m(List<Integer>), because they have the same erasure.List<String> strings = new ArrayList<>();
List raw = strings; // raw type: unchecked
raw.add(42); // compiles (with a warning)…
String s = strings.get(0); // …ClassCastException HERE, far from the real bug
Key points to cover:
TypeReference<List<Order>> and Spring's ResolvableType work.Short answer: Arrays are reified and covariant. They know their element type at runtime, and check every store (ArrayStoreException). Generics are erased, so the runtime couldn't check that an element really is a List<String>. Allowing new List<String>[10] would let wrong types slip in with no error at all, so the compiler forbids it.
// List<String>[] arr = new List<String>[10]; // compile error
List<?>[] ok = new List<?>[10]; // unbounded wildcard arrays are allowed
List<List<String>> better = new ArrayList<>(); // prefer collections to arrays of generics
@SuppressWarnings("unchecked")
T[] items = (T[]) new Object[capacity]; // the common internal workaround (as ArrayList does)
Short answer: Always pass-by-value. For primitives, the value is copied. For objects, the reference is copied. So a method can mutate the object through its copy of the reference, but can't make the caller's variable point to a different object.
static void rename(Customer c) { c.setName("Ravi"); } // mutates the shared object: the caller sees it
static void replace(Customer c) { c = new Customer("Meera"); } // reassigns the LOCAL copy: the caller doesn't see it
Customer x = new Customer("Asha");
rename(x); // x.getName() → "Ravi"
replace(x); // x.getName() → still "Ravi"
Common trap: "objects are passed by reference". If that were true, replace would change x. The classic proof is that you can't write a working swap(a, b) method for object references.
Short answer: Imports are purely a compile-time convenience. They let you use simple names instead of fully qualified ones. They don't appear in the bytecode as instructions, don't load classes, and have no runtime cost. A class is loaded only when it's first actively used at runtime, whether or not it was imported.
Key points to cover:
java.util.*) don't import sub-packages, and aren't slower at runtime. Teams avoid them for readability, and to prevent ambiguity when a new class with the same name appears.import and import static?Short answer: import brings types into scope by simple name. import static brings a class's static members (methods and constants) into scope, so you can call max(a, b) instead of Math.max(a, b).
import static org.assertj.core.api.Assertions.assertThat; // the typical, idiomatic use: test DSLs
import static java.util.concurrent.TimeUnit.SECONDS;
assertThat(order.total()).isEqualByComparingTo("499.00");
executor.awaitTermination(30, SECONDS);
Short answer: Used for well-known DSL-style APIs (AssertJ, Mockito, Hamcrest, Collectors.*, TimeUnit), they make code read naturally. Overused, they hide where a method comes from, clash with local names, and make code reviews harder. The guideline: static-import only widely recognised members, and avoid wildcard static imports in production code.
Short answer: In an e-commerce checkout:
Cart object holds CartItems.CheckoutService (a Spring singleton) turns the cart into an Order, asks a PaymentGateway (interface → RazorpayGateway object) to charge it, saves the order through an OrderRepository, and publishes an OrderPlaced event that an EmailNotifier handles.Order order = cart.toOrder(customer); // domain objects encapsulate the rules
PaymentResult result = paymentGateway.charge(order); // collaborating through an interface
if (result.successful()) {
orders.save(order.markPaid(result.reference()));
events.publishEvent(new OrderPlaced(order.id()));
}
Learn it in depth → Object-Oriented Design Refresher
this be used in a static method or a static block?Short answer: No. this refers to the current instance, and static methods and static initialisers run without one. They belong to the class, and run even when no object exists. Using this (or super) there is a compile error.
Key points to cover:
new App().run().Q: What are bounded wildcards, and what is PECS?
A: ? extends T means "some subtype of T": you can read Ts from it (a Producer). ? super T means "some supertype of T": you can write Ts into it (a Consumer). "Producer Extends, Consumer Super". For example, Collections.copy(List<? super T> dest, List<? extends T> src).
Q: Why is ordinal() dangerous to persist?
A: Reordering or inserting enum constants changes their ordinals, silently corrupting stored data. Persist name() (JPA @Enumerated(EnumType.STRING)), or an explicit stable code field.
Q: What does var do, and is it dynamic typing?
A: var (Java 10) is local variable type inference. The compiler infers a static type from the initialiser. The type is fixed at compile time, so it isn't dynamic typing.
Q: Can you get the actual type argument of a generic at runtime?
A: Not from an instance (new ArrayList<String>() is just an ArrayList). But you can read it from declarations: a subclass (class OrderList extends ArrayList<Order>), fields and method signatures all keep it, through getGenericSuperclass() and friends.