Modern Java

Records, sealed interfaces, pattern matching, and lambdas.

At a Glance

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

AllowedNot allowed
Implement interfacesExtend a class
Static fields and methodsMutable instance fields
Custom constructors (must delegate)Non-final components
Override accessorsDeclare 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?

BenefitDetail
Exhaustive switchCompiler knows all subtypes — no default needed.
Domain modelingExpress "one of these, nothing else" in the type system.
Safe refactoringAdding 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

KindSyntaxLambda Equivalent
StaticInteger::parseInts -> Integer.parseInt(s)
Bound instanceSystem.out::printlns -> System.out.println(s)
Unbound instanceString::toUpperCases -> s.toUpperCase()
ConstructorArrayList::new() -> new ArrayList<>()

Common Functional Interfaces

From java.util.function. These are the type targets for lambdas.

InterfaceSignatureUse 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());