GAP·MAP

Modern Java (records, sealed, patterns)

[ middle depth ]

What the compiler generates for a record

A record header lists the components, and from record Point(int x, int y) {} the compiler writes a private final field per component, a public accessor named after each component (so p.x(), never getX()), a canonical constructor matching the header, and value-based equals, hashCode, and toString. Two records with equal components are equal and hash alike, which makes them safe as map keys and set elements. A record is implicitly final and already extends java.lang.Record, so it cannot extend another class, though it can implement any number of interfaces. You cannot add extra instance fields; only static members, methods, and nested types are allowed.

Compact constructors for validation

Validation and normalization go in a compact constructor, which drops the parameter list and the field assignments:

record Range(int lo, int hi) {
    Range {                              // compact form: no (int lo, int hi) header, no body assignment
        if (lo > hi) throw new IllegalArgumentException("lo > hi");
        lo = Math.max(lo, 0);            // reassign the PARAMETER to normalize
    }                                    // this.lo and this.hi are assigned automatically here
}

Wrong "I set the fields with this.lo = lo; at the end of the compact constructor." That is a compile error. A compact constructor never assigns this.field; it may reassign the parameters, and the compiler copies the final parameter values into the fields when the block ends. Right validate, optionally reassign a parameter, and let the implicit assignment run.

Sealed types and pattern matching

A sealed type fixes the set of permitted subtypes, and each one must be declared final, sealed, or non-sealed:

sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}   // a record is implicitly final, so it qualifies
record Square(double s) implements Shape {}

An instanceof pattern binds and casts in one step, and the binding is in scope only where the compiler can prove the match:

if (obj instanceof String s && s.length() > 3) { use(s); }   // s is bound after &&; null never matches

A switch over a sealed type can use type patterns or record patterns and needs no default once every permitted subtype is covered. A when clause adds a boolean guard, as in case Integer i when i > 0 -> "positive".

Switch expressions without fall-through

A switch expression produces a value, and arrow labels carry no fall-through and need no break:

int days = switch (month) {
    case FEB -> leap ? 29 : 28;
    case APR, JUN, SEP, NOV -> 30;       // one label, several constants
    default -> {
        int d = 31;
        yield d;                         // a block returns its value with yield
    }
};                                       // the whole switch is an expression, hence the ;

Wrong "I return a value from a block with break days;." break belongs to switch statements; a switch expression uses yield. Because it must produce a value on every path, a switch expression has to be exhaustive: cover every enum constant or add a default.

Text blocks and var

A text block is a plain String written between """ delimiters, and the opening """ must be followed by a line break:

String json = """
    {"id": 1}
    """;                                 // incidental leading indentation is stripped

The compiler removes the common leading indentation (measured against the closing """) and all trailing whitespace on each line. Local type inference with var reads well when the right side names the type, as in var users = new ArrayList<User>(). It applies only to local variables, loop variables, and try-with-resources, needs an initializer (var x = null; does not compile), and is never allowed on fields, method parameters, or return types.

[ senior depth ]

The version timeline interviews probe

Java ships a feature as preview, then final, and the exact final release is a common screen. Records were finalized in JDK 16 (JEP 395), not 17; only sealed classes and interfaces reached final in JDK 17 (JEP 409). Pattern matching for instanceof was final in 16, switch expressions in 14, text blocks in 15, and var has existed since JDK 10. Pattern matching for switch (JEP 441) and record patterns (JEP 440) were finalized in JDK 21. Java 17 and 21 are the LTS releases, which is why the range gets framed as 17 to 21; every feature here is available by 21.

Records are only shallowly immutable

The most common record trap is treating the object as deeply frozen. Components are final references, not deep copies. A record Data(int[] arr) {} freezes the field but not the array, and the generated accessor returns the same array, so any caller can mutate its contents. Real immutability needs a copy in and a copy out:

record Data(int[] arr) {
    Data { arr = arr.clone(); }                 // copy in (reassign the parameter)
    public int[] arr() { return arr.clone(); }  // copy out through an overridden accessor
}

Deserialization runs the canonical constructor, so these invariants hold for deserialized instances too, which makes records immune to the classic constructor-bypass attack. Records also forbid extra instance fields, and a nested or local record is implicitly static.

Sealed hierarchies and exhaustiveness

permits names the direct subtypes, and each one must be declared final, sealed, or non-sealed; leaving the modifier off is a compile error, and a record satisfies the rule by being implicitly final. Permitted subtypes must be accessible to the sealed type and live in the same module (for a named module) or the same package (on the classpath), though not necessarily the same file. You may drop the permits clause only when all subtypes sit in one source file, where the compiler infers them.

Wrong "A sealed type's subclasses have to all be in one file." That holds only when permits is omitted. The payoff is exhaustiveness: a switch covering every permitted subtype compiles with no default, and the compiler also rejects impossible casts across the closed hierarchy.

Failure modes of a pattern switch

Four sharp edges show up under review. Ordering is checked: a broad type pattern placed before a more specific one is a dominated, unreachable label and a compile error, so specific and guarded cases come first. Null is opt-in: a switch on a null selector throws NullPointerException unless a case null exists, and case null may only pair with default (case null, default), never with a type pattern. The selector may be any reference type or int, but not long, float, double, or boolean. An exhaustive switch over a sealed or record type can throw java.lang.MatchException at runtime if that hierarchy was recompiled after the switch, and the fix is recompiling the switch rather than adding a default.

var and text block edge cases

var infers the static type of the initializer and nothing more, so var list = new ArrayList<>() becomes ArrayList<Object> because a bare diamond has no target type to constrain it. var is a reserved type name, not a keyword: you cannot name a class var, yet a variable or method called var still compiles, and the language stays statically typed. It hurts readability when the right side is opaque, as in var r = service.process(x), where the reader loses the type. Text blocks carry their own edges: content cannot begin on the opening """ line, each line's trailing whitespace is stripped (write \s to keep one), and a trailing \ continues the line by removing the newline.

beta

The interviewer part is in the works.

The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.

builds on

more Java

was this useful?