Modern Java Features
A tour of Java's evolution from version 9 to 21: immutable collections, local type inference, records, sealed classes, pattern matching, text blocks, virtual threads, and the complete switch expression. Each feature is placed in context of the problem it solves.
Java's Accelerated Release Cycle
For many years Java released major versions slowly, roughly every two to three years. Starting with Java 9, Oracle moved to a six-month release cadence. A new version ships every March and September. Most versions are feature releases with a six-month support window. Long-Term Support (LTS) versions, currently Java 8, 11, 17, and 21, receive extended support for production use.
Features often enter the language as preview first, meaning they are functional and usable but may change before becoming final. Requiring --enable-preview to compile and run is the signal that a feature is in preview. Most of the features covered in this tutorial graduated from preview to final, and the ones in production use today on LTS releases (17 and 21) are all stable.
Java 9: Convenience Factory Methods and More
Java 9 was one of the largest releases in years, introducing the module system (JPMS), the interactive JShell REPL, private interface methods, and a set of factory methods that made creating small immutable collections vastly less painful.
Immutable Collection Factory Methods
Before Java 9, creating a small immutable list required either multiple add() calls, an anonymous subclass with a double-brace initializer (a well-known anti-pattern), or wrapping an Arrays.asList() call with Collections.unmodifiableList(). All of these were verbose. List.of(), Set.of(), and Map.of() each create a truly unmodifiable collection in one call. They reject null elements, and Set.of() rejects duplicate elements at construction time. The iteration order of Set.of() and Map.of() is intentionally unspecified and can change between JVM runs, which discourages code that accidentally relies on insertion order.
Java 9: List.of(), Set.of(), Map.of(), and Private Interface Methods
JavaImmutable collection creation, Map.ofEntries() for larger maps, and private methods in interfaces.
Java 9: JPMS and JShell
Two other major Java 9 features you will encounter in practice.
- JPMS (Java Platform Module System):Introduced module-info.java, which declares a module's name, what it exports, and what it requires. Modules enforce stronger encapsulation than packages: even public types in a non-exported package are inaccessible to code in other modules. Large applications and the JDK itself are now organized as modules.
- JShell:An interactive Read-Evaluate-Print Loop (REPL) for Java. Run jshell on the command line to type Java expressions and statements and see results immediately, without writing a full class. Excellent for quick experiments, learning, and testing API behaviour.
Java 10: var and copyOf()
Java 10 introduced local variable type inference with the var keyword. This is not dynamic typing. The compiler still infers a concrete, static type from the right-hand side expression. Once inferred, the type is fixed for the lifetime of the variable. You cannot reassign a different type to a var variable any more than you can to a traditionally declared one.
The design goal was to reduce the visual noise of long generic type declarations. Writing var entries = new HashMap<String, List<Integer>>() is more readable than spelling out the full type on both sides.var has deliberate restrictions. It cannot be used for method parameters, return types, or fields. It cannot infer the type when the right-hand side is a bare null literal (no concrete type exists). And it should not be overused: using var where the inferred type is not obvious from the right-hand side hurts readability more than it helps.
Java 10: var and copyOf()
JavaType inference with var in different contexts, and creating immutable copies of existing collections.
Java 11: New String Methods and File Utilities (LTS)
Java 11 is an important LTS release with several practical additions to String and Filesthat fill long-standing gaps.
The new String methods address common annoyances. isBlank() checks whether a string is empty or contains only whitespace, which is a common validation need that previously required trim().isEmpty(). strip() is the Unicode-aware replacement for trim(): it removes all Unicode whitespace, not just ASCII characters below \u0020. lines() returns a Stream<String> of lines, lazily split by line terminators, and repeat(n) returns the string concatenated to itself n times.
Java 11: String Methods and Files Utilities
JavaisBlank, strip, stripLeading, stripTrailing, lines, repeat, and the convenient Files.readString / writeString.
Java 14: Switch Expressions and Records (Preview)
Switch Expressions
The old switch statement has two well-known problems: accidental fall-through between cases, and the inability to produce a value. You could not write String label = switch (day) {...}and assign the result. You had to declare a variable before the switch and assign inside each case. That is several extra lines for a simple mapping.
Switch expressions fix both problems. The new arrow label syntax ( case X ->) does not fall through: each arm is isolated. The switch can produce a value that you assign directly. For arms that need multiple statements, the yield keyword returns the value from a block body. The compiler also enforces exhaustiveness for switch expressions: if you switch on an enum, every constant must have a case, or you must include a default.
Records
Before records, writing a simple data class in Java meant declaring fields, writing a constructor, writing getters, and implementing equals(), hashCode(), and toString(). That was easily 30 to 50 lines for a class that conceptually held two or three values. Records express the intent in one line. A record declaration record Point(int x, int y) {} gives you a final, immutable class with all of the above generated automatically. The components become private final fields, and accessor methods (not JavaBean-style getters, but methods named the same as the field: point.x(), point.y()) are generated.
Records can implement interfaces, define extra methods, add compact constructors for validation, and have static factory methods. They cannot extend other classes (they implicitly extend java.lang.Record) and they cannot add instance fields beyond the declared components.
Java 14-16: Switch Expressions and Records
JavaArrow-label switch expressions producing values, yield from blocks, and record classes with compact constructors.
Java 15-16: Text Blocks and Pattern Matching instanceof
Text Blocks
Writing multi-line strings in Java before text blocks was unpleasant: every newline required an explicit \n, embedded double quotes required escaping, and the whole thing was a concatenated mess that looked nothing like the actual content it represented.
Text blocks use a triple-double-quote delimiter. The opening """ is followed by a newline; the content starts on the next line. The closing """ determines the indentation baseline: the compiler strips leading whitespace from every line up to the column of the closing delimiter. This means you can indent the text block content to match your source code's indentation without that indentation appearing in the string value.
Pattern Matching for instanceof
The old way to check a type and then use the object as that type required two lines: the instanceof check and then an explicit cast. The pattern matching form merges them: if (obj instanceof String s) checks the type and, if it matches, binds the object to s as a String in scope. No separate cast is needed. The scope of the binding variable follows the flow of the boolean condition, so the compiler tracks where the check was proven true and makes the binding available exactly in those branches.
Java 15-16: Text Blocks and Pattern Matching instanceof
JavaMulti-line strings as text blocks with controlled indentation, and type-pattern binding in instanceof.
Java 17: Sealed Classes (LTS)
A sealed class or interface restricts which classes can extend or implement it. Without sealed, any class in any package can subclass your public abstract class. If you want to model a closed, exhaustive set of types (an algebraic data type in functional programming terms), there was no good way to enforce that boundary before Java 17.
The sealed keyword combined with permits declares exactly which classes are allowed to extend it. Those permitted subclasses must each be declared final (no further subclassing), sealed (continuing the restriction with their own permitted subclasses), or non-sealed (reopening the hierarchy to unrestricted subclassing at that level).
The practical value of sealed classes becomes clear with switch expressions. Because the compiler knows the complete set of permitted subtypes, it can enforce exhaustiveness in a switch that matches on subtypes, eliminating the need for a defensive default case.
Java 17: Sealed Classes and Exhaustive Switch
JavaModeling a payment system with a sealed hierarchy and pattern matching switch that the compiler verifies for exhaustiveness.
Java 21: Virtual Threads, Pattern Matching Switch, Record Patterns, and Sequenced Collections (LTS)
Virtual Threads (Project Loom)
Traditional Java threads map one-to-one to OS threads. OS threads are expensive: each carries a stack of roughly 1 MB and the OS can schedule only a few thousand of them before performance degrades. This is why HTTP servers use thread pools with fixed sizes of a few hundred threads. When a thread blocks on I/O, the OS keeps the thread alive and its stack in memory while it waits for a response from a database or remote service.
Virtual threads turn this model around. A virtual thread is a JVM-managed thread with a tiny initial stack (a few hundred bytes, growing dynamically). When a virtual thread performs a blocking operation, the JVM automatically unmounts it from its carrier platform thread. The platform thread is immediately available to run another virtual thread. When the blocking call completes, the virtual thread is mounted on any available platform thread and resumes. This means you can have millions of concurrent virtual threads with the same amount of memory that once supported only thousands of platform threads. The programming model stays the same: you still write sequential-looking code with blocking I/O calls.
Pattern Matching for Switch and Record Patterns
Two final features in Java 21 extend the power of pattern matching started in Java 16.
- Pattern matching for switch:case labels can now be type patterns, not just constants. case Circle c -> handles Circles; case Rectangle r -> handles Rectangles. Null can be matched explicitly with case null. Guarded patterns add conditions: case Circle c when c.radius() > 10.
- Record patterns:You can deconstruct a record directly in a pattern. if (obj instanceof Point(int x, int y)) binds x and y without calling p.x() or p.y(). This composes with switch and with nested records for deep deconstruction in a single expression.
- Sequenced Collections:A new interface hierarchy (SequencedCollection, SequencedSet, SequencedMap) adds getFirst(), getLast(), addFirst(), addLast(), and reversed() to List, Deque, LinkedHashSet, and LinkedHashMap. Accessing the first or last element of a LinkedHashMap no longer requires awkward iterator hacks.
Java 21: Virtual Threads
JavaCreating millions of virtual threads, comparing memory usage versus platform threads, and the simple API for virtual thread creation.
Java 21: Pattern Matching Switch, Record Patterns, and Sequenced Collections
JavaType patterns in switch with guards, deconstructing records directly in patterns, and the new Sequenced Collections API.
Quiz - Test Your Knowledge
Ten questions covering the features introduced across Java 9 through Java 21: immutable collections, var type inference, new String methods, switch expressions, records, text blocks, pattern matching, sealed classes, virtual threads, and sequenced collections. Read each option carefully before selecting your answer.
Knowledge Check
1. What is the key difference between List.of() introduced in Java 9 and Arrays.asList()?
2. What does the var keyword in Java 10 actually do?
3. What does the enhanced switch expression (Java 14+) offer that the old switch statement did not?
4. What makes a Java Record different from a regular class?
5. What does "sealed class" mean in Java 17?
6. What does "pattern matching for instanceof" (Java 16+) remove from your code?
7. What is the main benefit of Virtual Threads introduced in Java 21?
8. What does a Text Block provide compared to ordinary string literals?
9. What is a Sequenced Collection in Java 21?
10. What does the var keyword NOT work with?