Java · Lesson 15 of 20

Modern Java: var, switch expressions, text blocks, pattern matching, sealed classes

Write modern Java 21+ code: var, switch expressions, text blocks, pattern matching for instanceof and switch, record patterns and sealed classes.

  • Intermediate
  • 20 min read
  • 4 objectives

Before this lessonLesson 14: Lambdas and Streams

What you will learn

  • Use var for local type inference where it improves readability
  • Write switch expressions and text blocks
  • Apply pattern matching with instanceof, switch and record patterns
  • Model closed hierarchies with sealed interfaces

Your Progress

0 of 20 lessons 0%

  • Lessons0 / 20
  • Completed0
  • Est. time left~ 5 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

Java has changed a lot since Java 8. A new version ships every six months, and the long-term support releases (Java 17, 21 and 25) bundle features that make everyday code shorter and safer. If you learned Java from older tutorials, many of these features will look new, yet they are now standard in production code.

This lesson tours the most useful ones. They combine nicely: by the end you will model data with records and sealed interfaces and process it with a pattern-matching switch, a style often called data-oriented programming. Everything here is final (not preview) in Java 21 and later.

var: local type inference

Since Java 10, you can write var instead of a type for local variables when the type is obvious from the right-hand side. The variable is still statically typed; the compiler just works out the type for you. It removes noise from long generic types, but do not use it where the type would become a mystery to the reader.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        var names = new ArrayList<String>();          // ArrayList<String>
        names.add("Ada");
        names.add("Linus");

        var scores = new HashMap<String, List<Integer>>();
        scores.put("Ada", List.of(90, 95));

        for (var entry : scores.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        var count = names.size();                      // int
        System.out.println(count + " " + ((Object) count).getClass().getSimpleName());
        // names = 42;   // compile error: still an ArrayList<String>
    }
}
Output
Ada -> [90, 95]
2 Integer

Switch expressions

The old switch statement falls through to the next case unless you remember break, a classic bug. The arrow form (case X ->) never falls through, lets you list several labels per case, and can be used as an expression that produces a value. For multi-line cases, yield returns the value.

public class Main {
    static String dayType(String day) {
        return switch (day) {
            case "SAT", "SUN" -> "weekend";
            case "MON", "TUE", "WED", "THU", "FRI" -> "weekday";
            default -> throw new IllegalArgumentException("unknown day: " + day);
        };
    }

    static int shippingCost(String region, double weightKg) {
        return switch (region) {
            case "IN" -> 5;
            case "US", "EU" -> {
                int base = 10;
                yield weightKg > 2 ? base + 8 : base;
            }
            default -> 25;
        };
    }

    public static void main(String[] args) {
        System.out.println(dayType("SAT") + ", " + dayType("WED"));
        System.out.println(shippingCost("US", 3.5) + " " + shippingCost("IN", 1) + " " + shippingCost("BR", 1));
    }
}
Output
weekend, weekday
18 5 25

Text blocks

Multi-line strings for JSON, SQL or HTML used to be a mess of \n and +. A text block (Java 15+) starts with """ and a newline. Java strips the common indentation, so you can indent it with your code. formatted() fills in placeholders.

public class Main {
    public static void main(String[] args) {
        String sql = """
            SELECT id, email
            FROM users
            WHERE plan = 'pro'
            ORDER BY id""";
        System.out.println(sql);

        String json = """
            {
              "name": "%s",
              "lessons": %d
            }
            """.formatted("Java", 20);
        System.out.print(json);
    }
}
Output
SELECT id, email
FROM users
WHERE plan = 'pro'
ORDER BY id
{
  "name": "Java",
  "lessons": 20
}

Pattern matching for instanceof

Before Java 16, checking a type meant testing and then casting on a separate line. Now instanceof can declare a pattern variable that is already cast and in scope wherever the test is known to be true, including in the rest of an && condition.

public class Main {
    static String describe(Object value) {
        if (value instanceof String s && !s.isBlank()) {
            return "text of length " + s.length();
        } else if (value instanceof Integer i) {
            return "number, doubled " + (i * 2);
        }
        return "something else";
    }

    public static void main(String[] args) {
        System.out.println(describe("stackcone"));
        System.out.println(describe(21));
        System.out.println(describe(3.14));
    }
}
Output
text of length 9
number, doubled 42
something else

Pattern matching in switch

Java 21 made switch work on types, not just constants. Each case can match a type pattern, add a when guard for extra conditions, and handle null explicitly. Cases are checked top to bottom, so put more specific patterns first.

import java.util.*;

public class Main {
    static String format(Object o) {
        return switch (o) {
            case null -> "null!";
            case Integer i when i < 0 -> "negative int " + i;
            case Integer i -> "int " + i;
            case String s when s.isEmpty() -> "empty string";
            case String s -> "string \"" + s + "\"";
            case List<?> list -> "list of " + list.size();
            default -> "other: " + o.getClass().getSimpleName();
        };
    }

    public static void main(String[] args) {
        List<Object> inputs = new ArrayList<>(List.of(-4, 7, "", "hi", List.of(1, 2), 2.5));
        inputs.add(null);
        for (Object in : inputs) System.out.println(format(in));
    }
}
Output
negative int -4
int 7
empty string
string "hi"
list of 2
other: Double
null!

Sealed classes and record patterns

A sealed interface or class lists exactly which types may implement it with permits. That tells the compiler the hierarchy is closed, so a switch over it needs no default: if you later add a new shape and forget to handle it, the code stops compiling instead of failing at runtime. Record patterns go further and pull a record's components straight into variables. The unnamed variable _ (final in Java 22) ignores components you do not need.

public class Main {
    sealed interface Shape permits Circle, Rectangle, Triangle {}
    record Circle(double radius) implements Shape {}
    record Rectangle(double width, double height) implements Shape {}
    record Triangle(double base, double height) implements Shape {}

    static double area(Shape shape) {
        return switch (shape) {                   // no default needed: all cases covered
            case Circle(double r) -> Math.PI * r * r;
            case Rectangle(double w, double h) -> w * h;
            case Triangle(double b, double h) -> 0.5 * b * h;
        };
    }

    static String kind(Shape shape) {
        return switch (shape) {
            case Rectangle(var w, var h) when w == h -> "square";
            case Rectangle _ -> "rectangle";
            case Circle _, Triangle _ -> "round or pointy";
        };
    }

    public static void main(String[] args) {
        Shape[] shapes = { new Circle(1), new Rectangle(2, 3), new Rectangle(4, 4), new Triangle(6, 2) };
        for (Shape s : shapes) {
            System.out.printf("%-32s area=%6.2f  %s%n", s, area(s), kind(s));
        }
    }
}
Output
Circle[radius=1.0]               area=  3.14  round or pointy
Rectangle[width=2.0, height=3.0] area=  6.00  rectangle
Rectangle[width=4.0, height=4.0] area= 16.00  square
Triangle[base=6.0, height=2.0]   area=  6.00  round or pointy

This combination (records for data, sealed interfaces for the set of variants, pattern-matching switch for behaviour) is how modern Java models things like API responses, payment results or parsed commands.

Other small wins worth knowing

  • List.of, Set.of, Map.of (Java 9) create compact immutable collections.
  • Helpful NullPointerExceptions (Java 14+) name exactly which variable was null.
  • Sequenced collections (Java 21) add getFirst(), getLast() and reversed() to lists and ordered sets.
  • Virtual threads (Java 21) make blocking concurrent code cheap; see the concurrency lesson.
  • Java 25 simplifies small programs with compact source files: void main() { IO.println("hi"); } runs without a class declaration.

Recap

  • var infers local variable types; use it when the type is obvious.
  • Arrow-form switch expressions return values, never fall through and support yield.
  • Text blocks make multi-line strings readable; formatted() fills placeholders.
  • Pattern matching with instanceof and switch (with when guards) removes manual casts.
  • Sealed interfaces plus records and record patterns give exhaustive, compiler-checked data models.
// Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 16Dates and Times with java.timeWork with dates and times in Java using java.time: LocalDate, LocalDateTime, Instant, ZonedDateTime, Duration, Period, formatting and parsing.