Why lambdas changed Java, generics for type safety and reuse, what streams really do for performance and maintainability, default methods and API evolution, interfaces vs abstract classes after Java 8, @Retention and @Target, the diamond problem, the recursive Enum<E extends Enum<E>> declaration, records and sealed classes.
Published September 25, 2026
Senior candidates are expected to discuss language features in terms of design impact. How does this feature change the APIs you design, the bugs you can prevent, and the performance you get? Get the Java versions right too (preview versus final), because interviewers notice.
Short answer: Lambdas made behaviour a first-class value. That changed how Java libraries are designed, not just how code is written:
CompletableFuture, Comparator.comparing, Map.computeIfAbsent, and Spring's callback-style APIs.invokedynamic, so they're cheap. Non-capturing lambdas are singletons, and the JIT can inline them.Learn it in depth → Lambdas & Functional Interfaces
Short answer: Generics move type checks to compile time. A List<Order> can't receive a Customer, and reading from it needs no cast, so a whole class of runtime ClassCastExceptions disappears. One implementation serves every type, and bounded type parameters (<T extends Comparable<? super T>>) express constraints precisely.
public interface Repository<T, ID> { // one abstraction for every entity
Optional<T> findById(ID id);
T save(T entity);
}
public static <T extends Comparable<? super T>> T max(Collection<? extends T> items) { // PECS
return items.stream().max(Comparator.naturalOrder()).orElseThrow();
}
Key points to cover:
new T(), no generic arrays, and no instanceof List<String>. Work around them with Class<T> tokens, or supertype tokens.Learn it in depth → Generics
Short answer: Maintainability: pipelines state the intent (filter, map, group) declaratively, compose well, and keep side effects out of view. Performance is mostly neutral for sequential streams: slightly slower than hand-written loops for tiny workloads, and comparable after JIT warm-up. It improves with primitive streams, with short-circuiting, and sometimes with parallel streams, on large, CPU-bound, easily split data.
Common trap: "streams make code faster because they run in parallel". Parallelism is opt-in. It uses the shared common ForkJoinPool, and often hurts in server code: small datasets, blocking I/O, contention with the request threads. Measure with JMH.
Short answer:
Collection.stream()).Comparator.reversed, Predicate.and).Design implications:
Learn it in depth → Default Methods & Optional
Short answer: Default methods blurred the line, but the key difference remains state and construction.
A common combination is an interface for the contract plus an abstract skeletal implementation (AbstractList).
Key points to cover:
@Retention for?Short answer: It declares how long an annotation survives:
SOURCE: discarded by the compiler. Used for compile-time tools: @Override, Lombok, annotation processors.CLASS (the default): kept in the bytecode, but not visible through reflection. Used by bytecode tools.RUNTIME: available through reflection at runtime. Required for Spring, JPA, Jackson and Bean Validation annotations, and for custom annotations read by aspects.@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited { String action(); }
@Around("@annotation(audited)") // readable at runtime only because of RUNTIME retention
Object audit(ProceedingJoinPoint jp, Audited audited) throws Throwable { … }
Common trap: forgetting RUNTIME on a custom annotation. The aspect or reflection code then silently never sees it.
@Target do?Short answer: It restricts where an annotation may be used: TYPE, METHOD, FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE (any use of a type, as in List<@NonNull String>), MODULE and RECORD_COMPONENT. Misplacing the annotation becomes a compile error.
Key points to cover:
@Documented (appears in the Javadoc);@Inherited (class annotations inherited by subclasses);@Repeatable.@RestController is built from @Controller + @ResponseBody using ANNOTATION_TYPE.Short answer: The implementing class must override the method. Otherwise it's a compile error. Inside the override, delegate explicitly with InterfaceName.super.method(), combine both, or write new behaviour.
interface Loggable { default String describe() { return "loggable"; } }
interface Auditable { default String describe() { return "auditable"; } }
final class Invoice implements Loggable, Auditable {
@Override public String describe() { return Auditable.super.describe(); } // an explicit, documented choice
}
Key points to cover:
Enum class?Short answer: It's declared as public abstract class Enum<E extends Enum<E>>, a recursive (F-bounded) generic. Each enum Color compiles to final class Color extends Enum<Color>, so methods inherited from Enum are typed to the concrete enum:
compareTo(E o) only accepts the same enum type (you can't compare a Color with a Size).getDeclaringClass() returns Class<E>.valueOf(Class<T>, String) and EnumSet<E extends Enum<E>> / EnumMap<K extends Enum<K>, V> get compile-time type safety.Common trap: saying it's declared Enum<?>. The wildcard Enum<?> is how you refer to "some enum" in your own code. The declaration is the self-referential bound. The same pattern appears in fluent builders (Builder<T extends Builder<T>>).
Short answer: A record (a preview in Java 14–15, final in Java 16) is a transparent, shallowly immutable data carrier. You declare its components, and the compiler generates the private final fields, the canonical constructor, the accessors (name(), not getName()), and equals, hashCode and toString. Records can have compact constructors for validation or normalisation, static factories, instance methods, and they can implement interfaces.
public record Money(BigDecimal amount, Currency currency) {
public Money { // compact constructor
Objects.requireNonNull(currency);
if (amount.scale() > currency.getDefaultFractionDigits()) throw new IllegalArgumentException("bad scale");
}
public Money plus(Money other) { return new Money(amount.add(other.amount), currency); }
}
Key points to cover:
@Embeddables (Hibernate 6.2+).List.copyOf.Learn it in depth → Records
Short answer: A sealed class or interface (a preview in Java 15–16, final in Java 17) restricts which classes may extend or implement it, with a permits clause. Each permitted subclass must be final, sealed or non-sealed. That gives you a closed hierarchy, so the compiler knows every variant. Pattern-matching switch (Java 21) can then be exhaustive without a default, and adding a new variant turns every non-exhaustive switch into a compile error.
public sealed interface PaymentResult permits Approved, Declined, Pending { }
public record Approved(String reference) implements PaymentResult { }
public record Declined(String reason) implements PaymentResult { }
public record Pending(Duration retryAfter) implements PaymentResult { }
String message(PaymentResult r) {
return switch (r) { // exhaustive: no default needed
case Approved a -> "Paid (" + a.reference() + ")";
case Declined d -> "Declined: " + d.reason();
case Pending p -> "Processing, retry in " + p.retryAfter().toSeconds() + "s";
};
}
Key points to cover:
Learn it in depth → Sealed Classes
Q: What are record patterns?
A: Java 21 lets you deconstruct records in instanceof and switch: case Approved(var ref) -> …, including nested patterns. Combined with sealed types, this enables concise data-oriented code.
Q: Can records have mutable fields?
A: Their component fields are final, but a component can reference a mutable object (a List, an array). Defensive copies in the compact constructor keep a record effectively immutable.
Q: What's the difference between non-sealed and final for a permitted subclass?
A: final closes the hierarchy at that point. non-sealed reopens it, so anyone can extend that subclass. It's a deliberate escape hatch.
Q: When would you write an annotation processor?
A: To generate code or validate usage at compile time (like MapStruct, Lombok or Dagger). It needs SOURCE or CLASS retention, and it gives zero runtime reflection cost.