Modern Java
Records, sealed interfaces, pattern matching, and lambdas.
At a Glance
record(Java 16) — Immutable data carriers with auto-generatedequals,hashCode,toString, and accessors. Replace boilerplate POJOs. Cannot extend classes, but can implement interfaces.sealed(Java 17) — Restrict which classes can extend/implement a type. Enables exhaustive pattern matching. Use for closed type hierarchies like AST nodes or domain events.- Pattern matching for
instanceof(Java 16) — Combines type check and cast into one expression. Eliminates redundant casts afterinstanceof. - Pattern matching for
switch(Java 21) — Match on types, deconstruct records, and use guarded patterns in switch expressions. Exhaustiveness checked by compiler for sealed types. - Record patterns (Java 21) — Deconstruct record components directly in
instanceofandswitch. Enables nested destructuring. - Lambdas (Java 8) — Anonymous functions for functional interfaces. Foundation for the Streams API and modern Java idioms. Captured variables must be effectively final.
- Method references (Java 8) — Shorthand for lambdas that just call an existing method. Four forms: static, instance, arbitrary instance, and constructor.
- Functional interfaces (Java 8) — Single-abstract-method interfaces like
Function,Predicate,Consumer,Supplier. The type targets for lambdas and method references.
Records
Immutable data carriers. Java 16+.
// Basic record — generates constructor, accessors, equals, hashCode, toString
record Point(int x, int y) {}
Point p = new Point(3, 4);
p.x(); // 3 (accessor, not getX)
p.toString(); // "Point[x=3, y=4]"
// Compact constructor — validate without repeating params
record Range(int lo, int hi) {
Range {
if (lo > hi) throw new IllegalArgumentException("lo > hi");
}
}
// Records can implement interfaces
record NamedPoint(String name, int x, int y) implements Serializable {}
// Static methods, instance methods are fine
record Email(String value) {
static Email of(String s) { return new Email(s.toLowerCase()); }
String domain() { return value.substring(value.indexOf('@') + 1); }
} Restrictions
| Allowed | Not allowed |
|---|---|
| Implement interfaces | Extend a class |
| Static fields and methods | Mutable instance fields |
| Custom constructors (must delegate) | Non-final components |
| Override accessors | Declare additional instance fields |
Sealed Interfaces & Classes
Closed type hierarchies. Java 17+.
// Only these three can implement Shape
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double a, double b, double c) implements Shape {}
// Subtypes must be: final, sealed, or non-sealed
sealed class Animal permits Dog, Cat {}
final class Dog extends Animal {}
non-sealed class Cat extends Animal {} // open for further extension Why sealed?
| Benefit | Detail |
|---|---|
| Exhaustive switch | Compiler knows all subtypes — no default needed. |
| Domain modeling | Express "one of these, nothing else" in the type system. |
| Safe refactoring | Adding a new permitted subtype forces callers to handle it. |
Pattern Matching
Type checks, casts, and deconstruction in one step.
instanceof (Java 16+)
// Old way
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// Pattern matching — cast is implicit
if (obj instanceof String s) {
System.out.println(s.length());
}
// Works with && (binding in scope on right side)
if (obj instanceof String s && s.length() > 5) {
System.out.println(s);
} switch (Java 21+)
// Type patterns in switch
String describe(Object obj) {
return switch (obj) {
case Integer i -> "int: " + i;
case String s -> "string: " + s;
case null -> "null";
default -> "other: " + obj;
};
}
// Exhaustive switch on sealed types — no default needed
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> heronsFormula(t.a(), t.b(), t.c());
};
}
// Guarded patterns
String classify(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "large circle";
case Circle c -> "small circle";
case Rectangle r -> "rectangle";
case Triangle t -> "triangle";
};
} Record Patterns (Java 21+)
// Deconstruct record components directly
if (p instanceof Point(int x, int y)) {
System.out.println(x + ", " + y);
}
// Nested deconstruction
record Line(Point start, Point end) {}
String describeLine(Object obj) {
return switch (obj) {
case Line(Point(var x1, var y1), Point(var x2, var y2))
-> "(%d,%d)-(%d,%d)".formatted(x1, y1, x2, y2);
default -> "not a line";
};
} Lambdas
Anonymous functions for functional interfaces. Java 8+.
// Full syntax
Comparator<String> cmp = (String a, String b) -> { return a.compareTo(b); };
// Inferred types, expression body
Comparator<String> cmp = (a, b) -> a.compareTo(b);
// Single parameter — parens optional
Function<String, Integer> len = s -> s.length();
// No parameters
Runnable r = () -> System.out.println("hello");
// Multi-line body
Consumer<List<String>> printer = items -> {
for (String item : items) {
System.out.println(item);
}
}; Method References
| Kind | Syntax | Lambda Equivalent |
|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance | System.out::println | s -> System.out.println(s) |
| Unbound instance | String::toUpperCase | s -> s.toUpperCase() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
Common Functional Interfaces
From java.util.function. These are the type targets for lambdas.
| Interface | Signature | Use Case |
|---|---|---|
Function<T, R> | R apply(T t) | Transform a value |
Predicate<T> | boolean test(T t) | Filter / condition |
Consumer<T> | void accept(T t) | Side effect (logging, saving) |
Supplier<T> | T get() | Lazy value / factory |
UnaryOperator<T> | T apply(T t) | Transform same type |
BinaryOperator<T> | T apply(T t1, T t2) | Combine two values (reduce) |
BiFunction<T, U, R> | R apply(T t, U u) | Two-arg transform |
BiConsumer<T, U> | void accept(T t, U u) | Two-arg side effect |
// Composing functions
Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> pipeline = trim.andThen(upper);
pipeline.apply(" hello "); // "HELLO"
// Composing predicates
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> startsWithA = s -> s.startsWith("A");
List<String> result = names.stream()
.filter(notEmpty.and(startsWithA))
.toList(); Variable Capture
Lambdas can capture local variables, but they must be effectively final.
// OK — name is effectively final
String name = "Alice";
Runnable r = () -> System.out.println(name);
// Compile error — count is modified
int count = 0;
Runnable r = () -> System.out.println(count); // error
count++;
// Workaround — use an array or AtomicInteger
AtomicInteger count = new AtomicInteger(0);
Runnable r = () -> System.out.println(count.incrementAndGet());