Search Tutorials


1Z0-830 Java SE 21 - New Features | JavaInUse

1Z0-830 New Features - Java SE 21 Certification Prep

The 1Z0-830 exam covers language and API features introduced across Java versions 8 through 21. This page summarizes the most exam-relevant features grouped by the version that made them standard. Features that were previewed in earlier versions are listed under the version where they became final. Pay particular attention to records, sealed classes, pattern matching for switch, and virtual threads, as these are heavily tested in the Java 21 exam.

Java 8 Features

Java 8 was a landmark release that introduced functional programming support into the language. The features it added remain foundational to modern Java code and are prerequisites for understanding everything that followed.
Key Java 8 Features:
  • Lambda expressions - Concise syntax for implementing functional interfaces inline, without creating an anonymous class.
  • Functional interfaces - Interfaces with exactly one abstract method, which can be implemented by a lambda or method reference.
  • Stream API - A pipeline model for processing sequences of elements with operations like filter, map, reduce, and collect.
  • Default and static methods in interfaces - Allow interfaces to provide method implementations, enabling interface evolution without breaking existing implementors.
  • Optional class - A container that may or may not hold a value, used to avoid returning null from methods.
  • New Date/Time API (java.time) - Immutable, thread-safe replacement for the error-prone java.util.Date and Calendar classes.
// Lambda expressions - implement a functional interface without an anonymous class
Comparator<String> comp = (s1, s2) -> s1.compareTo(s2);
list.forEach(item -> System.out.println(item));

// Functional interfaces - exactly one abstract method
@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}
Calculator add = (a, b) -> a + b;
int sum = add.calculate(3, 4);  // 7

// Method references - shorthand for lambdas that simply call an existing method
list.forEach(System.out::println);        // Instance method on a fixed instance
strings.stream().map(String::length);     // Instance method on the stream element
Arrays.sort(array, Integer::compare);     // Static method
Supplier<List<String>> s = ArrayList::new; // Constructor reference

// Default methods - provide a default implementation in the interface
// Implementing classes inherit this but can override it
interface Vehicle {
    default void start() {
        System.out.println("Starting vehicle");
    }
}

// Static methods in interfaces - called on the interface, not on instances
interface MathUtils {
    static int add(int a, int b) {
        return a + b;
    }
}
int result = MathUtils.add(2, 3);

// Stream pipeline - lazy evaluation, only processes what is needed
List<String> filtered = list.stream()
    .filter(s -> s.length() > 3)      // Intermediate - keeps elements matching predicate
    .map(String::toUpperCase)           // Intermediate - transforms each element
    .sorted()                           // Intermediate - sorts all elements
    .collect(Collectors.toList());      // Terminal - triggers evaluation and collects results

// Optional - signals that a return value may be absent
Optional<String> opt = Optional.ofNullable(value);  // Empty if value is null
String r = opt.orElse("default");                    // Return value or fallback
opt.ifPresent(System.out::println);                  // Run action only if value present
String upper = opt.map(String::toUpperCase)
                  .orElse("EMPTY");                  // Transform then get, or fallback

// java.time API - immutable and thread-safe
LocalDate date = LocalDate.now();
LocalTime time = LocalTime.of(14, 30);
LocalDateTime dt = LocalDateTime.now();
ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("America/New_York"));
Duration d = Duration.ofHours(2);     // Time-based amount (hours, minutes, seconds)
Period p = Period.ofDays(30);         // Date-based amount (years, months, days)

Java 9 Features

Java 9 added convenience factory methods for collections, several Stream and Optional enhancements, and the ability to define private methods in interfaces. The collection factory methods are particularly important for the exam because of their immutability constraints and null-rejection behavior.
// Collection factory methods - create compact IMMUTABLE collections
// These are value-based: no guaranteed identity, order may vary for Set and Map
List<String> list = List.of("a", "b", "c");
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("a", 1, "b", 2);

// Map.ofEntries - use when you have more than 10 entries (Map.of only goes to 10 pairs)
Map<String, Integer> map2 = Map.ofEntries(
    Map.entry("a", 1),
    Map.entry("b", 2)
);

// All mutation methods throw UnsupportedOperationException
list.add("d");      // UnsupportedOperationException
set.remove(1);      // UnsupportedOperationException
map.put("c", 3);    // UnsupportedOperationException

// Null elements and null keys are not allowed
List.of(null);              // NullPointerException
Map.of(null, 1);            // NullPointerException

// Duplicate elements in Set or duplicate keys in Map are not allowed
Set.of(1, 1);               // IllegalArgumentException at creation time
Map.of("a", 1, "a", 2);     // IllegalArgumentException at creation time

// Stream.takeWhile - emits elements while predicate is true, then stops
Stream.of(1, 2, 3, 4, 5)
    .takeWhile(n -> n < 4)  // 1, 2, 3  (stops when 4 fails the predicate)
    .forEach(System.out::println);

// Stream.dropWhile - skips elements while predicate is true, then emits the rest
Stream.of(1, 2, 3, 4, 5)
    .dropWhile(n -> n < 4)  // 4, 5  (skips 1, 2, 3 then emits everything from 4 onward)
    .forEach(System.out::println);

// Stream.ofNullable - returns a single-element stream, or empty stream if null
// Avoids a NullPointerException when the source value may be null
Stream<String> stream = Stream.ofNullable(value); // Empty if value is null

// Optional enhancements
opt.stream();                        // Convert Optional to a Stream of 0 or 1 element
opt.or(() -> Optional.of("alt"));    // Return alternative Optional if empty
opt.ifPresentOrElse(
    val -> System.out.println(val),  // Called if value present
    () -> System.out.println("Empty") // Called if empty
);

// Private methods in interfaces
// Allow default methods to share helper logic without exposing it
interface MyInterface {
    default void publicMethod() {
        helperMethod();  // Calls the private helper
    }

    private void helperMethod() {
        System.out.println("Helper logic shared by default methods");
    }
}
Exam Tip: List.of(), Set.of(), and Map.of() create IMMUTABLE collections that reject null values and throw UnsupportedOperationException on any mutation attempt. Set.of() and Map.of() also reject duplicate elements or keys at creation time with IllegalArgumentException. The iteration order of Set.of() and Map.of() is not guaranteed and may vary between JVM runs.

Java 10 Features

Java 10 introduced var for local variable type inference, which lets the compiler infer the type from the initializer expression. This reduces verbosity without sacrificing type safety - the variable is still statically typed, the type is just written by the compiler instead of by you.
// var - local variable type inference
// The compiler infers the type from the right-hand side expression
var list   = new ArrayList<String>();     // Inferred as ArrayList<String>
var map    = new HashMap<String, Integer>(); // Inferred as HashMap<String, Integer>
var stream = list.stream();               // Inferred as Stream<String>

// var in enhanced for loop
for (var item : list) {
    System.out.println(item.toUpperCase()); // Compiler knows item is String
}

// var in traditional for loop
for (var i = 0; i < 10; i++) {
    System.out.println(i);
}

// Restrictions - var is only valid where the type can be unambiguously inferred

// Must have an initializer
var x;              // Compile error - no initializer, type cannot be inferred

// Cannot be initialized to null alone - null has no type
var y = null;       // Compile error - type of null is unknown

// Not allowed for method parameters
void method(var param) { }   // Compile error

// Not allowed for instance or class fields
class MyClass {
    var field = 10;  // Compile error - var is only for local variables
}

// Not allowed in catch clauses or as method return type
// catch (var e) { }  // Compile error

// Immutable copy methods (Java 10)
// Returns a new unmodifiable collection containing all elements of the original
List<String> copy    = List.copyOf(original);
Set<Integer> setCopy = Set.copyOf(originalSet);
Map<String, Integer> mapCopy = Map.copyOf(originalMap);
// If original already is unmodifiable and contains no nulls, may return same instance

// Collectors for unmodifiable collections
List<String> immutable = stream
    .collect(Collectors.toUnmodifiableList());
Map<String, Long> immutableMap = stream
    .collect(Collectors.toUnmodifiableMap(
        Function.identity(), s -> (long) s.length()));
Exam Tip: var is NOT a keyword - it is a reserved type name. This means you can still use var as a variable name: var var = "value"; is valid Java 10 code (the first var is the type inference token, the second is the variable name). You cannot, however, use var as a class name because it would conflict with the reserved type name context.

Java 11 Features

Java 11 was the first long-term support release after Java 8. It added several practical String utility methods, allowed var in lambda parameters (primarily to support annotations on lambda parameters), and added convenience methods to Files and collections.
// var in lambda parameters - mainly useful for adding annotations to parameters
// Both parameters must use var if one does (cannot mix var and explicit types)
BiFunction<String, String, String> concat = (var a, var b) -> a + b;

// Primary use case: annotations on lambda parameters (not possible without var)
// (@NonNull var a, @NotEmpty var b) -> a + b

// New String methods
String s = "  Hello World  ";

s.isBlank();            // false - true only if string is empty or all whitespace
"   ".isBlank();        // true
"".isBlank();           // true

// strip() is Unicode-aware; trim() only handles ASCII whitespace (chars <= '\u0020')
// Prefer strip() over trim() in new code
s.strip();              // "Hello World"   - removes leading and trailing whitespace
s.stripLeading();       // "Hello World  " - removes leading whitespace only
s.stripTrailing();      // "  Hello World" - removes trailing whitespace only

"hello\nworld\nfoo".lines();  // Returns Stream<String> of "hello", "world", "foo"
"ab".repeat(3);               // "ababab"

// Files convenience methods (also available in later versions)
Path path = Path.of("file.txt");
String content = Files.readString(path);           // Entire file as String, UTF-8
Files.writeString(path, "content");                // Write String to file, UTF-8
Files.writeString(path, "more", StandardOpenOption.APPEND);

// Collection.toArray with generator function - type-safe array creation
List<String> list = List.of("a", "b", "c");
String[] array = list.toArray(String[]::new);  // No unchecked cast needed

// Predicate.not() - static factory for a negated predicate
// Cleaner alternative to s -> !s.isBlank() when using method references
list.stream()
    .filter(Predicate.not(String::isBlank))
    .collect(Collectors.toList());

// Optional.isEmpty() - explicit check for empty Optional
Optional<String> opt = Optional.empty();
opt.isEmpty();    // true  - complement of isPresent()
opt.isPresent();  // false

Java 14-15 Features

Switch expressions became standard in Java 14, replacing the old switch statement syntax with a more concise and safer form. Text blocks became standard in Java 15, providing a readable way to embed multiline strings in source code. Both features significantly reduce boilerplate and are frequently tested on the exam.
// Switch expressions (standard in Java 14)
// Can be used on the right side of an assignment or as a method return value
int day = 3;

String dayType = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";   // Multiple labels in one case
    case 6, 7 -> "Weekend";
    default -> "Invalid";
};

// yield - returns a value from a block case (required when using braces)
String result = switch (day) {
    case 1, 2, 3, 4, 5 -> "Weekday";
    case 6, 7 -> {
        System.out.println("It is the weekend!");
        yield "Weekend";  // yield is mandatory here to provide the switch value
    }
    default -> "Invalid";
};

// Switch expressions must be exhaustive - all possible input values must be handled
// For enums, covering all constants eliminates the need for a default
enum Season { SPRING, SUMMER, FALL, WINTER }

String describeSeason(Season s) {
    return switch (s) {
        case SPRING -> "Flowers";
        case SUMMER -> "Hot";
        case FALL   -> "Leaves";
        case WINTER -> "Snow";
        // No default needed - compiler verifies all four enum values are covered
    };
}

// Text blocks (standard in Java 15)
// Opening """ must be on a line by itself followed by a newline
// Indentation of the closing """ determines how much leading whitespace is stripped
String json = """
    {
        "name": "Alice",
        "age": 30,
        "city": "Boston"
    }
    """;
// Closing """ is at column 4, so 4 spaces of leading whitespace are stripped from each line

// Text blocks preserve HTML without escaping angle brackets
String html = """
    <html>
        <body>Hello</body>
    </html>
    """;

// New escape sequences for text blocks:
// \ at end of a line suppresses the newline (joins the line with the next)
String singleLine = """
    Hello \
    World\
    """;  // Produces: "Hello World"

// \s at the end of a line preserves trailing spaces (prevents whitespace trimming)
String withSpaces = """
    line1   \s
    line2   \s
    """;  // Each line ends with three spaces
Exam Tip: In switch expressions, the arrow syntax (case X ->) does NOT fall through to the next case. The colon syntax (case X:) still falls through by default, just like in the old switch statement. Use yield to return a value from a block case - returning from a block with return exits the enclosing method, not the switch. Switch expressions used as a statement (result discarded) still must be exhaustive if the selector type is an enum.

Java 16 Features

Records became standard in Java 16, providing a concise syntax for immutable data carrier classes. Pattern matching for instanceof also became standard, eliminating the cast that previously followed a type check. Both features work together naturally and are central to the Java 21 pattern matching story.
// Records (standard in Java 16)
// A record is a transparent, immutable data carrier
public record Point(int x, int y) { }

// The compiler automatically generates for you:
// - private final fields named x and y
// - A canonical constructor that sets both fields
// - Public accessor methods x() and y()  (NOT getX() / getY())
// - equals() based on all components
// - hashCode() based on all components
// - toString() such as "Point[x=10, y=20]"

Point p = new Point(10, 20);
int x = p.x();   // 10 - accessor method, not a field access
int y = p.y();   // 20

// Compact constructor - validates or normalizes component values
// Parameters are implicit; you do not declare them
public record Person(String name, int age) {
    public Person {
        if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
        name = name.strip();  // Can reassign the implicit parameters before they are stored
        // Fields are assigned from the (possibly modified) parameters after the body exits
    }
}

// What records CAN do:
// - Define static fields and static methods
// - Define additional instance methods
// - Implement interfaces
// - Define custom constructors (must delegate to the canonical one)
// - Override individual accessor methods or toString/equals/hashCode

// What records CANNOT do:
// - Extend another class (records implicitly extend java.lang.Record)
// - Be extended (records are implicitly final)
// - Declare additional instance fields (only the components declared in the header)
// - Be abstract

public record Employee(String name) implements Comparable<Employee> {
    static int instanceCount = 0;  // Static field - OK

    public String greeting() {
        return "Hello, " + name;   // Additional instance method - OK
    }

    @Override
    public int compareTo(Employee other) {
        return name.compareTo(other.name);
    }
}

// Pattern matching for instanceof (standard in Java 16)
Object obj = "Hello";

// Before Java 16: explicit cast after type check
if (obj instanceof String) {
    String s = (String) obj;        // Redundant cast
    System.out.println(s.length());
}

// Java 16+: pattern variable bound in the same expression
if (obj instanceof String s) {
    System.out.println(s.length()); // s is already cast and in scope
}

// Pattern variable scope: s is only in scope where the match is guaranteed true
if (obj instanceof String s && s.length() > 5) {
    // s in scope here - both conditions must be true to reach this block
}

// Negated pattern: s is in scope in the else path
if (!(obj instanceof String s)) {
    return;  // obj is not a String
}
System.out.println(s.length());  // s is in scope here - the non-String case returned early
Exam Tip: Record accessor methods have the same name as the component - no "get" prefix. point.x() not point.getX(). The compact constructor does not list parameters - they are implicit and available for validation. After the compact constructor body, the fields are assigned from whatever values the parameters hold at that point, so you can normalize them by reassigning the parameters inside the body.

Java 17 Features (LTS)

Sealed classes became standard in Java 17. A sealed class or interface explicitly lists which classes are allowed to extend or implement it. This gives the compiler a complete, closed set of subtypes, which enables exhaustive pattern matching without a default case. Sealed classes work especially well with records and pattern matching for switch.
// Sealed class - only the listed classes in permits may extend it
public sealed class Shape
    permits Circle, Rectangle, Triangle { }

// Each permitted subclass must be final, sealed, or non-sealed
public final class Circle extends Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    public double radius() { return radius; }
}

// A permitted subclass can itself be sealed, restricting its own subtypes
public sealed class Rectangle extends Shape
    permits Square {
    public double width()  { return 0; }  // Illustrative
    public double height() { return 0; }
}

public final class Square extends Rectangle { }

// non-sealed opens the hierarchy back up - any class can extend Triangle
public non-sealed class Triangle extends Shape {
    public double base()   { return 0; }
    public double height() { return 0; }
}

// Rules for sealed classes:
// 1. All permitted subclasses must directly extend the sealed class
// 2. Each permitted subclass must be final, sealed, or non-sealed
// 3. Permitted subclasses must be in the same module as the sealed class
//    If no module, they must be in the same package (or same compilation unit)
// 4. The permits clause can be omitted if all subclasses are in the same source file

// Sealed interfaces - same rules apply to implementing classes/records
public sealed interface Vehicle
    permits Car, Truck, Motorcycle { }

public final class Car implements Vehicle { }

// Records are implicitly final, so they satisfy the final requirement
public record Truck(int capacity) implements Vehicle { }

// The real power of sealed classes: exhaustive pattern matching in switch
// The compiler knows all subtypes of Shape, so no default is needed
double getArea(Shape shape) {
    return switch (shape) {
        case Circle c    -> Math.PI * c.radius() * c.radius();
        case Rectangle r -> r.width() * r.height();
        case Triangle t  -> 0.5 * t.base() * t.height();
        // No default case needed - compiler verifies all Shape subtypes are covered
    };
}
Exam Tip: The three valid modifiers for permitted subclasses are final, sealed, and non-sealed. A subclass that is neither of these three is a compile error. The permits clause is optional only when all permitted subclasses are declared in the same source file as the sealed class. If the subclasses are in separate files, the permits clause is required.

Java 21 Features (LTS)

Java 21 is a long-term support release that made several major features standard: pattern matching for switch, record patterns (deconstruction), sequenced collections, and virtual threads. These features work together to enable concise and expressive code for data-oriented programming. Pattern matching for switch and record patterns are the most exam-intensive of the four.
// Pattern matching for switch (standard in Java 21)
// The selector can be any reference type or primitive wrapper
Object obj = "Hello";

String result = switch (obj) {
    case Integer i -> "Integer: " + i;
    case String s  -> "String: " + s;
    case Long l    -> "Long: " + l;
    case null      -> "Null value";   // Explicit null case prevents NullPointerException
    default        -> "Unknown: " + obj;
};

// Guarded patterns - add a boolean condition with the "when" keyword
// More specific guarded patterns must come before less specific ones
String format(Object obj) {
    return switch (obj) {
        case String s when s.isEmpty()       -> "Empty string";
        case String s when s.length() < 10  -> "Short: " + s;
        case String s                        -> "Long: " + s;  // catch-all for String
        case Integer i when i < 0           -> "Negative: " + i;
        case Integer i                       -> "Non-negative: " + i;
        default                              -> "Unknown";
    };
}

// Record patterns - deconstruct a record's components in a pattern
record Point(int x, int y) { }
record Line(Point start, Point end) { }

// Deconstruct directly in instanceof
void printPoint(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println("x=" + x + ", y=" + y);  // x and y extracted directly
    }
}

// Nested record patterns - deconstruct records within records
void printLine(Object obj) {
    if (obj instanceof Line(Point(int x1, int y1),
                            Point(int x2, int y2))) {
        System.out.println("From (" + x1 + "," + y1 +
                           ") to (" + x2 + "," + y2 + ")");
    }
}

// Record patterns combined with guarded switch
String describePoint(Object obj) {
    return switch (obj) {
        case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
        case Point(int x, int y) when x == 0            -> "On Y axis";
        case Point(int x, int y) when y == 0            -> "On X axis";
        case Point(int x, int y)                        -> "Point at " + x + "," + y;
        default                                         -> "Not a point";
    };
}

// Virtual Threads (standard in Java 21)
// Virtual threads are lightweight threads managed by the JVM, not the OS.
// They are designed for I/O-bound workloads where threads spend most time waiting.
// You can create millions of virtual threads without exhausting OS resources.

// Create and start a single virtual thread
Thread vThread = Thread.ofVirtual().start(() -> {
    System.out.println("Running in virtual thread: " + Thread.currentThread().isVirtual());
});

// Preferred approach: one virtual thread per task via an executor
// The executor is AutoCloseable - try-with-resources waits for all tasks to complete
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1000; i++) {
        executor.submit(() -> {
            // Each task runs on its own virtual thread
            // Blocking I/O here is cheap - the JVM unmounts the virtual thread while waiting
            return fetchDataFromNetwork();
        });
    }
}  // Executor closes here, waiting for all 1000 tasks to finish

// Named virtual threads (useful for debugging and monitoring)
ThreadFactory factory = Thread.ofVirtual()
    .name("worker-", 0)  // Threads named worker-0, worker-1, worker-2...
    .factory();

// Check whether current thread is virtual
boolean isVirtual = Thread.currentThread().isVirtual();

// Sequenced Collections (standard in Java 21)
// New interfaces that expose first/last element access on ordered collections

// SequencedCollection - extends Collection; for List, Deque, LinkedHashSet
SequencedCollection<String> seq = new ArrayList<>();
seq.addFirst("first");                          // Insert at the front
seq.addLast("last");                            // Insert at the back
String first = seq.getFirst();                  // Read front element
String last  = seq.getLast();                   // Read back element
seq.removeFirst();                              // Remove and return front element
seq.removeLast();                               // Remove and return back element
SequencedCollection<String> reversed = seq.reversed(); // View in reverse order

// SequencedSet - extends SequencedCollection and Set
// Implemented by LinkedHashSet and TreeSet
SequencedSet<String> orderedSet = new LinkedHashSet<>();
orderedSet.getFirst();
orderedSet.getLast();
orderedSet.reversed();

// SequencedMap - extends Map; for LinkedHashMap and TreeMap
// Provides access to the first and last entries in encounter order
SequencedMap<String, Integer> orderedMap = new LinkedHashMap<>();
Map.Entry<String, Integer> fe = orderedMap.firstEntry();  // Does not remove
Map.Entry<String, Integer> le = orderedMap.lastEntry();
orderedMap.pollFirstEntry();     // Removes and returns first entry
orderedMap.pollLastEntry();      // Removes and returns last entry
orderedMap.putFirst("a", 1);     // Insert or move entry to front
orderedMap.putLast("z", 26);     // Insert or move entry to back
SequencedMap<String, Integer> rm = orderedMap.reversed();
Exam Tip: In pattern switch, case ordering matters for guarded patterns and type patterns involving inheritance - the compiler rejects unreachable cases. A more specific guarded case (like case String s when s.isEmpty()) must appear before a less specific one (like case String s). Virtual threads are not faster than platform threads for CPU-bound work - their advantage is that they are cheap to create and block without tying up an OS thread, making them ideal for high-concurrency I/O workloads. Do not use synchronized blocks in virtual thread code if you can avoid it - use ReentrantLock instead, because a virtual thread that blocks inside a synchronized block pins its carrier thread.

1Z0-830 Java SE 21 Certification - Table of Contents

Master all exam topics with comprehensive study guides and practice examples.


Popular Posts