Skip to content
Laviyx Learn

Try

Java course menu

Modern Java Features (Java 10 to 21): var, Records, Text Blocks, Switch Expressions and Pattern Matching

A tour of modern Java: var, enhanced switch expressions, text blocks, records, pattern matching for instanceof and switch, record patterns, sealed classes, Optional, the java.time API, new String and collection methods, and sequenced collections in Java 21.

Advanced14 min readUpdated
On this page

Java has changed a lot since Java 8. Code written with modern features is shorter, safer and easier to read than the style found in many older tutorials. This lesson gathers the most useful additions up to the Java 21 LTS release, several of which you met earlier in the course.

VersionHighlights
Java 8Lambdas, streams, Optional, java.time
Java 9–11List.of, var, new String methods (isBlank, strip, repeat)
Java 14–16Switch expressions, records, pattern matching for instanceof, text blocks
Java 17 (LTS)Sealed classes
Java 21 (LTS)Pattern matching for switch, record patterns, virtual threads, sequenced collections

var#

var infers a local variable's type from its initializer. It removes noise from long generic types:

Java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Main {
    public static void main(String[] args) {
        var scores = new HashMap<String, List<Integer>>();   // instead of repeating the type
        scores.put("Asha", new ArrayList<>(List.of(92, 88)));

        for (var entry : scores.entrySet()) {
            System.out.println(entry.getKey() + " " + entry.getValue());
        }
    }
}
Output
Asha [92, 88]

Use var when the type is obvious from the right-hand side; write the type when it helps readers.

Switch expressions#

Java
public class Main {
    public static void main(String[] args) {
        int month = 4;
        String season = switch (month) {
            case 12, 1, 2 -> "winter";
            case 3, 4, 5 -> "summer";
            case 6, 7, 8, 9 -> "monsoon";
            default -> "autumn";
        };
        System.out.println(season);
    }
}
Output
summer

No fallthrough, no break, several labels per case, and the compiler checks exhaustiveness. See switch.

Text blocks#

Java
public class Main {
    public static void main(String[] args) {
        String name = "Asha";
        String html = """
            <p>
              Hello, %s!
            </p>
            """.formatted(name);
        System.out.print(html);
    }
}
Output
<p>
  Hello, Asha!
</p>

Records#

Java
record Money(String currency, long amount) {
    Money {
        if (amount < 0) throw new IllegalArgumentException("negative amount");
    }

    Money plus(Money other) {
        return new Money(currency, amount + other.amount);
    }
}

public class Main {
    public static void main(String[] args) {
        Money a = new Money("INR", 500);
        Money b = new Money("INR", 250);
        System.out.println(a.plus(b));
        System.out.println(a.equals(new Money("INR", 500)));
    }
}
Output
Money[currency=INR, amount=750]
true

See Records and Enums.

Pattern matching#

instanceof#

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

    public static void main(String[] args) {
        System.out.println(describe("hello"));
        System.out.println(describe(21));
        System.out.println(describe(3.5));
    }
}
Output
text of length 5
number 42 when doubled
something else

Sealed types, switch patterns and record patterns#

Together, these let you model data as a closed set of record types and process it safely. The compiler knows every possible case, so no default is needed, and record patterns take records apart in the case itself:

Java
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 {}

public class Main {
    static double area(Shape shape) {
        return switch (shape) {
            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;
        };
    }

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

If you add a new Shape to the permits list and forget a case, the switch stops compiling.

Optional#

Optional<T> represents a value that may be absent, making "not found" explicit instead of returning null:

Java
import java.util.Map;
import java.util.Optional;

public class Main {
    static final Map<String, String> EMAILS = Map.of("asha", "asha@example.com");

    static Optional<String> findEmail(String user) {
        return Optional.ofNullable(EMAILS.get(user));
    }

    public static void main(String[] args) {
        System.out.println(findEmail("asha").map(String::toUpperCase).orElse("unknown"));
        System.out.println(findEmail("john").orElse("unknown"));
        findEmail("asha").ifPresent(e -> System.out.println("send to " + e));
        System.out.println(findEmail("john").isPresent());
    }
}
Output
ASHA@EXAMPLE.COM
unknown
send to asha@example.com
false

Use Optional mainly as a return type. Avoid get() without checking; prefer orElse, orElseThrow, map and ifPresent.

Dates and times: java.time#

The java.time API (Java 8) replaced the old, error-prone Date and Calendar classes. Its types are immutable and clearly named:

Java
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Period;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;

public class Main {
    public static void main(String[] args) {
        LocalDate start = LocalDate.of(2026, 11, 2);
        LocalDate end = LocalDate.of(2027, 1, 15);

        System.out.println(ChronoUnit.DAYS.between(start, end));
        System.out.println(start.plusDays(30));
        System.out.println(start.getDayOfWeek() == DayOfWeek.MONDAY);
        System.out.println(Period.between(start, end));
        System.out.println(start.format(DateTimeFormatter.ofPattern("dd MMM yyyy")));
        System.out.println(LocalDate.parse("2026-10-23").isLeapYear());
    }
}
Output
74
2026-12-02
true
P2M13D
02 Nov 2026
false

LocalDate (a date), LocalTime, LocalDateTime, ZonedDateTime (with a time zone), Instant (a moment in UTC), Duration and Period cover nearly every need. Formatting month names uses your system's language settings unless you pass a Locale.

Handy small additions#

Java
import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        System.out.println("   ".isBlank() + " [" + "  hi  ".strip() + "] " + "ab".repeat(3));
        System.out.println("line1\nline2\n".lines().count());

        List<String> names = new ArrayList<>(List.of("Asha", "Ravi", "Meera"));
        System.out.println(names.getFirst() + " " + names.getLast());   // Java 21
        System.out.println(names.reversed());                           // Java 21

        List<Integer> squares = List.of(1, 2, 3).stream().map(n -> n * n).toList();   // Java 16
        System.out.println(squares);
    }
}
Output
true [hi] ababab
2
Asha Meera
[Meera, Ravi, Asha]
[1, 4, 9]

Java 21's sequenced collections add getFirst, getLast, addFirst, addLast and reversed to lists, deques, LinkedHashSet, LinkedHashMap and sorted collections.

Common mistakes#

Practice#

Practice exercise

Define a sealed interface Payment with records Card(String last4, double amount) and Upi(String handle, double amount). Write a method that uses a switch with record patterns to describe each payment, like Card ending 4242: 499.00, and use it on one of each.

Show solution
Java
sealed interface Payment permits Card, Upi {}
record Card(String last4, double amount) implements Payment {}
record Upi(String handle, double amount) implements Payment {}

public class Payments {
    static String describe(Payment p) {
        return switch (p) {
            case Card(String last4, double amount) -> String.format("Card ending %s: %.2f", last4, amount);
            case Upi(String handle, double amount) -> String.format("UPI %s: %.2f", handle, amount);
        };
    }

    public static void main(String[] args) {
        System.out.println(describe(new Card("4242", 499)));
        System.out.println(describe(new Upi("asha@bank", 1250.5)));
    }
}
Output
Card ending 4242: 499.00
UPI asha@bank: 1250.50

Key takeaway#

Finished this lesson?