Modern Language Features
Records, sealed types, pattern matching for instanceof and switch, record patterns, text blocks, and type inference.
7 questions
JuniorTheoryCommonHow does pattern matching for instanceof remove the cast after a type check?
How does pattern matching for instanceof remove the cast after a type check?
A type pattern binds a variable in the same test: if (obj instanceof String s) checks the type and, on success, assigns the already-cast value to s. The binding is in scope wherever the condition is provably true, so the separate explicit cast and the redundant second reference to the type both disappear.
Common mistakes
- ✗Expecting the binding to be in scope even on the branch where the
instanceoftest failed - ✗Writing a redundant explicit cast after the pattern, defeating the whole point of the feature
- ✗Thinking the binding is a fresh copy, when it references the same object viewed at the narrower type
Follow-up questions
- →How does flow scoping decide where the pattern variable
sis visible? - →Can you combine a type pattern with
&&conditions in the sameif, and why does that work?
JuniorTheoryCommonWhat boilerplate does a record eliminate, and what does the compiler generate?
What boilerplate does a record eliminate, and what does the compiler generate?
A record is a transparent carrier for immutable data. From its component list the compiler generates a private final field and an accessor per component, a canonical constructor, and value-based equals, hashCode, and toString, all derived from those components. You write none of them yet may override any.
Common mistakes
- ✗Thinking a record's fields are mutable, when each component becomes a
private finalfield with no setter - ✗Assuming you must still write
equals/hashCode, when the compiler derives them from the components - ✗Believing a record can extend another class, when records are implicitly
finaland extend onlyRecord
Follow-up questions
- →How would you validate or normalise a component inside a record's canonical constructor?
- →Why are records implicitly
final, and what does that mean for inheritance?
JuniorTheoryCommonWhat does the diamond operator <> infer, and where may var be used?
What does the diamond operator <> infer, and where may var be used?
The diamond <> lets the compiler infer a generic type's arguments from the assignment target, so List<String> xs = new ArrayList<>(); need not repeat them. var infers a local variable's type from its initialiser at compile time, and the variable then stays statically typed. var is legal only on a local with an initialiser — never on a field, a parameter or a return type.
Common mistakes
- ✗Thinking
varmakes a variable dynamically typed, when its type is fixed at compile time from the initialiser - ✗Trying to use
varon a field, on a method parameter, or on a declaration that has no initialiser - ✗Treating
new ArrayList<>()as equivalent to the rawnew ArrayList(), which loses all generic checking
Follow-up questions
- →What type is inferred for
var xs = new ArrayList<>();, and why does that surprise people? - →Why can
varnever appear on a field or on a method parameter?
MiddleTheoryCommonHow do type patterns and record patterns in switch replace an instanceof chain?
How do type patterns and record patterns in switch replace an instanceof chain?
Each case may carry a type pattern (case Circle c ->) that tests the type and binds the value in the label, optionally refined by a when guard. A record pattern destructures the record's components in that same label (case Point(int x, int y) ->), nesting recursively. Over a sealed hierarchy the compiler proves the switch exhaustive, so no default is needed; null matches only an explicit case null, otherwise the switch throws.
Common mistakes
- ✗Keeping a
defaultbranch over a sealed hierarchy, which silently swallows a newly added subtype - ✗Forgetting that a
nullselector throws unless the switch has an explicitcase nulllabel - ✗Writing a broader type pattern before a narrower one, which makes the later case unreachable
Follow-up questions
- →Why does a guarded
whencase not count towards a switch's exhaustiveness? - →How does pattern dominance decide the order in which the cases must be written?
JuniorTheoryOccasionalWhat is a text block, and how does the compiler decide its indentation?
What is a text block, and how does the compiler decide its indentation?
A text block is a multi-line string literal opened by """ and a line break, so embedded JSON, SQL or HTML keeps its shape without \n or escaped quotes. The compiler strips the incidental indentation — the white-space prefix common to every content line and to the closing delimiter — and removes trailing spaces. What remains is an ordinary String with no runtime cost.
Common mistakes
- ✗Thinking the position of the closing
"""does not matter, when it takes part in the incidental-indentation calculation - ✗Expecting a text block to interpolate variables, when it is only a literal and has no template syntax
- ✗Believing a text block yields some special type instead of a plain
String
Follow-up questions
- →How does moving the closing delimiter left or right change the resulting string?
- →What do the
\sescape and a trailing\do inside a text block?
MiddleTheoryOccasionalWhat does sealed restrict, and what must each permitted subtype declare?
What does sealed restrict, and what must each permitted subtype declare?
sealed restricts which types may extend or implement a type: the permits clause names them, and each named subtype must sit in the same module or package and be declared final, sealed or non-sealed. Because the full subtype set is then known at compile time, a switch over the hierarchy — typically over records — can be checked for exhaustiveness.
Common mistakes
- ✗Forgetting that every permitted subtype must itself be declared
final,sealedornon-sealed - ✗Placing a permitted subtype in a different module, which the compiler rejects
- ✗Adding a
defaultbranch anyway, which throws away the exhaustiveness the sealing bought
Follow-up questions
- →What does
non-sealeddo to a permitted subtype's own hierarchy? - →Why can a
switchover a sealed interface omit itsdefaultbranch?
SeniorDesignOccasionalA 400-class order service still writes Java 8 idioms: its DTOs are mutable JavaBeans with hand-written equals/hashCode, and its PaymentEvent hierarchy is dispatched by a 200-line if/else instanceof chain. The team has just moved to Java 21 and wants records, sealed types and pattern matching for switch. The constraints: the DTOs are serialised into a public API that paying third parties depend on; the release train ships weekly and cannot be blocked by a big-bang rewrite; several DTOs are still mutated in place by legacy mappers; and that if/else chain is today the only place a new event type is handled, so a forgotten subtype falls through silently. How would you sequence this migration, which classes would you convert first, and what would you put in place so that an untouched call site cannot break in silence?
A 400-class order service still writes Java 8 idioms: its DTOs are mutable JavaBeans with hand-written equals/hashCode, and its PaymentEvent hierarchy is dispatched by a 200-line if/else instanceof chain. The team has just moved to Java 21 and wants records, sealed types and pattern matching for switch. The constraints: the DTOs are serialised into a public API that paying third parties depend on; the release train ships weekly and cannot be blocked by a big-bang rewrite; several DTOs are still mutated in place by legacy mappers; and that if/else chain is today the only place a new event type is handled, so a forgotten subtype falls through silently. How would you sequence this migration, which classes would you convert first, and what would you put in place so that an untouched call site cannot break in silence?
Migrate leaf-first, never big-bang. Convert only the already-immutable DTOs into records, leaving the mutated ones behind a mapper until their callers stop mutating them. Seal the PaymentEvent hierarchy, then replace the instanceof chain with an exhaustive pattern switch: an unhandled subtype becomes a compile error, not a silent fall-through. Ship each step separately, pinned by the contract tests that guard the public API.
Common mistakes
- ✗Converting a DTO that legacy mappers still mutate in place, which a record cannot support
- ✗Keeping a
defaultbranch in the new patternswitch, which re-hides the unhandled subtype the sealing was meant to expose - ✗Treating a record's component names as an internal detail when they are part of the serialised public contract
Follow-up questions
- →How would you keep the published JSON shape stable while the DTO behind it becomes a record?
- →Why does adding a
defaultbranch defeat the very exhaustiveness you sealed the hierarchy for?