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>
}
}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));
}
}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);
}
}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));
}
}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));
}
}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));
}
}
}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()andreversed()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
varinfers 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
instanceofandswitch(withwhenguards) 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.
