Java New Features: Java 8 through Java 21+
A practical guide to the most impactful features introduced in modern Java versions โ from lambdas and streams (Java 8) to virtual threads and pattern matching (Java 21).
1. Java 8 (LTS) โ The Big Leapโ
Java 8 was a transformational release that introduced functional programming constructs to Java.
Lambda Expressionsโ
Anonymous function syntax for implementing functional interfaces:
// Before: anonymous inner class
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
};
// After: lambda
Comparator<String> comp = (a, b) -> a.length() - b.length();
Functional Interfacesโ
An interface with exactly one abstract method. Annotated with @FunctionalInterface:
| Interface | Method | Use Case |
|---|---|---|
Function<T, R> | R apply(T t) | Transform: T โ R |
Predicate<T> | boolean test(T t) | Filter: T โ boolean |
Consumer<T> | void accept(T t) | Side-effect: T โ void |
Supplier<T> | T get() | Factory: () โ T |
UnaryOperator<T> | T apply(T t) | Transform: T โ T |
BiFunction<T, U, R> | R apply(T t, U u) | Transform: (T, U) โ R |
Stream APIโ
Declarative pipeline for processing collections:
List<String> names = people.stream()
.filter(p -> p.getAge() > 18) // filter
.sorted(Comparator.comparing(Person::getName)) // sort
.map(Person::getName) // transform
.distinct() // remove duplicates
.limit(10) // take first 10
.collect(Collectors.toList()); // terminal operation
// Reduction
int totalAge = people.stream()
.mapToInt(Person::getAge)
.sum();
// Grouping
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::getCity));
// Parallel stream (use with caution)
long count = list.parallelStream()
.filter(s -> s.length() > 5)
.count();
Optionalโ
A container that may or may not hold a value. Eliminates explicit null checks:
// Creating
Optional<String> opt = Optional.of("value");
Optional<String> empty = Optional.empty();
Optional<String> nullable = Optional.ofNullable(mayBeNull);
// Using
String result = opt
.filter(s -> s.length() > 3)
.map(String::toUpperCase)
.orElse("default");
// Chaining
String city = getUser()
.flatMap(User::getAddress)
.map(Address::getCity)
.orElse("Unknown");
Date-Time API (java.time)โ
Replaces the problematic java.util.Date and Calendar:
// Immutable, thread-safe
LocalDate date = LocalDate.of(2024, 3, 15);
LocalTime time = LocalTime.of(14, 30);
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// Duration and Period
Duration duration = Duration.between(time1, time2);
Period period = Period.between(date1, date2);
// Formatting
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String formatted = dateTime.format(fmt);
LocalDateTime parsed = LocalDateTime.parse("2024-03-15 14:30", fmt);
Interface Default Methodsโ
Interfaces can now have method implementations:
public interface Collection<E> {
// Abstract method
boolean add(E e);
// Default method โ existing implementations don't break
default boolean isEmpty() {
return size() == 0;
}
// Static method
static <T> Collection<T> empty() {
return Collections.emptyList();
}
}
2. Java 9 โ Modularityโ
Module System (JPMS)โ
Organizes code into modules with explicit dependencies:
// module-info.java
module com.myapp {
requires java.sql;
requires java.logging;
exports com.myapp.api; // visible to other modules
opens com.myapp.internal; // accessible via reflection
}
JShell (REPL)โ
Interactive Java shell for experimenting:
jshell> int x = 42;
x ==> 42
jshell> "Hello".chars().sum()
$1 ==> 500
Collection Factory Methodsโ
List<String> list = List.of("a", "b", "c"); // immutable
Set<Integer> set = Set.of(1, 2, 3); // immutable
Map<String, Integer> map = Map.of("a", 1, "b", 2); // immutable
Other Notable Featuresโ
Optional.ifPresentOrElse(),Optional.stream()- Private interface methods
Stream.takeWhile(),Stream.dropWhile(),Stream.ofNullable()- G1 becomes the default garbage collector
- Compact Strings (internal
byte[]instead ofchar[]for Latin-1)
3. Java 10 โ Local Variable Type Inferenceโ
var keywordโ
Lets the compiler infer local variable types:
// Explicit type
ArrayList<Map<String, List<Integer>>> data = new ArrayList<>();
// With var โ much cleaner
var data = new ArrayList<Map<String, List<Integer>>>();
// Works in for loops
for (var entry : map.entrySet()) {
var key = entry.getKey();
var value = entry.getValue();
}
Rules:
- Only for local variables with initializers
- Not for method parameters, return types, or fields
- Not for
nullor lambda:var x = null;โvar f = () -> {};โ
4. Java 11 (LTS) โ HTTP Client & Moreโ
HttpClient APIโ
Modern, asynchronous HTTP client replacing HttpURLConnection:
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/data"))
.header("Accept", "application/json")
.GET()
.build();
// Synchronous
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Asynchronous
CompletableFuture<HttpResponse<String>> future =
client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
Other Featuresโ
varin lambda parameters:(var x, var y) -> x + y- String methods:
isBlank(),strip(),lines(),repeat(int) - Files methods:
Files.readString(path),Files.writeString(path, content) - ZGC (experimental) โ ultra-low-latency garbage collector
5. Java 12โ13 โ Switch Expressions & Text Blocksโ
Switch Expressions (Preview โ Standard in 14)โ
// Traditional switch โ verbose, fall-through prone
switch (day) {
case MONDAY:
case FRIDAY:
System.out.println("Work hard");
break;
case SATURDAY:
case SUNDAY:
System.out.println("Rest");
break;
}
// Switch expression โ concise, no fall-through
String activity = switch (day) {
case MONDAY, FRIDAY -> "Work hard";
case SATURDAY, SUNDAY -> "Rest";
default -> "Regular day";
};
Text Blocks (Preview โ Standard in 15)โ
Multi-line string literals:
// Before: messy concatenation
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30\n" +
"}";
// After: text blocks
String json = """
{
"name": "John",
"age": 30
}
""";
6. Java 14โ15 โ Records & Sealed Classesโ
Recordsโ
Immutable data carriers with auto-generated equals(), hashCode(), toString(), and accessors:
// Before: verbose POJO
public class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { /* ... */ }
@Override public int hashCode() { /* ... */ }
@Override public String toString() { /* ... */ }
}
// After: one line
public record Point(int x, int y) { }
// Usage
Point p = new Point(3, 4);
int x = p.x(); // accessor (not getX())
System.out.println(p); // Point[x=3, y=4]
Records can have:
- Custom constructors (compact or canonical)
- Instance methods
- Static fields and methods
- Implement interfaces
Records cannot: extend other classes, have mutable fields, be subclassed.
Sealed Classes (Preview โ Standard in 17)โ
Restrict which classes can extend or implement a type:
public sealed interface Shape permits Circle, Rectangle, Triangle { }
public record Circle(double radius) implements Shape { }
public record Rectangle(double width, double height) implements Shape { }
public final class Triangle implements Shape { /* ... */ }
Why sealed classes?
- Enables exhaustive pattern matching (compiler knows all subtypes)
- Models closed type hierarchies (algebraic data types)
7. Java 16 โ Pattern Matching for instanceofโ
Eliminates redundant casting:
// Before
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// After โ binding variable
if (obj instanceof String s) {
System.out.println(s.length()); // s is already cast
}
// Works with logical operators
if (obj instanceof String s && s.length() > 5) {
System.out.println(s.toUpperCase());
}
8. Java 17 (LTS) โ Sealed Classes Finalizedโ
Java 17 is a Long-Term Support release. Key finalized features:
- Sealed classes (from preview to standard)
- Pattern matching for
instanceof(standard) - Text blocks (standard)
- Records (standard)
- Strong encapsulation of JDK internals (cannot access internal APIs by default)
Migration Noteโ
Java 17 is the recommended upgrade target from Java 8 or 11. Key breaking changes:
- Strong encapsulation of
sun.misc.*APIs - Removed
SecurityManagerdeprecation - Removed RMI Activation
- Need
--add-opensfor frameworks using deep reflection
9. Java 21 (LTS) โ Virtual Threads & Pattern Matchingโ
Virtual Threads (Finalized)โ
Lightweight threads managed by the JVM, enabling massive concurrency:
// Create a virtual thread
Thread.ofVirtual().start(() -> {
System.out.println("Running in virtual thread");
});
// Virtual thread executor โ 1 virtual thread per task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Submit 100,000 tasks โ each gets a virtual thread
List<Future<String>> futures = IntStream.range(0, 100_000)
.mapToObj(i -> executor.submit(() -> fetchUrl("https://example.com/" + i)))
.toList();
}
Pattern Matching for switch (Finalized)โ
// Type patterns + guards
String describe(Object obj) {
return switch (obj) {
case Integer i when i > 0 -> "positive integer: " + i;
case Integer i -> "non-positive integer: " + i;
case String s -> "string of length " + s.length();
case null -> "null!";
default -> "something else";
};
}
// Sealed class exhaustiveness
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> t.base() * t.height() / 2;
// no default needed โ Shape is sealed, compiler knows all cases
};
}
Record Patterns (Finalized)โ
Deconstruct records in pattern matching:
record Point(int x, int y) { }
// Deconstruct directly
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
}
// Nested patterns in switch
switch (shape) {
case Circle(var radius) when radius > 10 -> "large circle";
case Circle(var radius) -> "small circle with radius " + radius;
default -> "not a circle";
}
Sequenced Collectionsโ
New interfaces for collections with defined encounter order:
// SequencedCollection
interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
void addFirst(E e);
void addLast(E e);
E getFirst();
E getLast();
E removeFirst();
E removeLast();
}
// Usage
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"
list.getLast(); // "c"
list.reversed(); // ["c", "b", "a"]
10. Feature Timeline Summaryโ
| Version | Year | Key Features | LTS? |
|---|---|---|---|
| 8 | 2014 | Lambdas, Streams, Optional, Date-Time API | โ |
| 9 | 2017 | Modules (JPMS), JShell, Collection factories | |
| 10 | 2018 | var local variable type inference | |
| 11 | 2018 | HttpClient, var in lambdas, ZGC (exp.) | โ |
| 12 | 2019 | Switch expressions (preview) | |
| 13 | 2019 | Text blocks (preview) | |
| 14 | 2020 | Records (preview), switch expressions (standard) | |
| 15 | 2020 | Sealed classes (preview), text blocks (standard) | |
| 16 | 2021 | Records (standard), pattern matching instanceof | |
| 17 | 2021 | Sealed classes (standard), strong encapsulation | โ |
| 18 | 2022 | UTF-8 default, simple web server | |
| 19 | 2022 | Virtual threads (preview), structured concurrency (incubator) | |
| 20 | 2023 | Virtual threads (2nd preview), scoped values (incubator) | |
| 21 | 2023 | Virtual threads, pattern matching switch, record patterns (all standard) | โ |
Recommended Upgrade Pathโ
Java 8 โ Java 17 (LTS) โ Java 21 (LTS)
- Java 8 โ 17: Biggest jump. Gain modules, records, sealed classes, text blocks, new GCs.
- Java 17 โ 21: Gain virtual threads, pattern matching for switch, record patterns.
11. Stream Internals โ How Pipelines Actually Executeโ
Understanding the Stream machinery separates senior developers from mid-level ones in interviews.
Lazy Evaluation and Pipelinesโ
Stream operations are split into intermediate (lazy) and terminal (eager):
- Intermediate:
filter(),map(),sorted(),distinct(),limit()โ builds a pipeline but does nothing - Terminal:
collect(),count(),findFirst(),forEach()โ triggers actual execution
// This code does NOTHING until collect() is called:
Stream<String> pipeline = names.stream()
.filter(n -> { System.out.println("filter: " + n); return n.length() > 3; })
.map(n -> { System.out.println("map: " + n); return n.toUpperCase(); });
// Execution only starts here โ and is interleaved element-by-element:
List<String> result = pipeline.collect(Collectors.toList());
// Output: filter: Alice, map: Alice, filter: Bob, filter: Charlie, map: Charlie
// (NOT: all filters first, then all maps)
Each element flows through the entire pipeline before the next element is processed โ this enables short-circuiting and minimizes memory use.
Short-Circuiting Optimizationโ
names.stream()
.filter(n -> n.length() > 3)
.findFirst(); // stops as soon as first match found โ doesn't process rest
limit(), findFirst(), findAny(), anyMatch(), allMatch(), noneMatch() all short-circuit.
Spliterator: The Engine Behind Streamsโ
A Spliterator is the iterator that powers streams, including parallel stream splitting:
// Custom object that can be split for parallel processing
Spliterator<String> spliterator = list.spliterator();
Spliterator<String> chunk = spliterator.trySplit(); // splits off a chunk for parallel work
Characteristics (bitmask flags on Spliterators):
ORDEREDโ encounter order is maintainedDISTINCTโ no duplicate elementsSORTEDโ elements are in sorted orderSIZEDโ known size upfront (enables parallelism optimizations)NONNULLโ no null elements
Parallel stream performance depends on these characteristics. SIZED + SUBSIZED lets ForkJoinPool split evenly. LinkedList (non-SIZED) splits poorly for parallel streams.
Common Senior Interview Trapsโ
// โ forEach ordering is undefined for parallel streams
list.parallelStream().forEach(System.out::println); // order not guaranteed
// โ
Use forEachOrdered for parallel + ordered
list.parallelStream().forEachOrdered(System.out::println);
// โ Stateful lambdas in parallel streams are dangerous
List<String> result = new ArrayList<>();
list.parallelStream().filter(...).forEach(result::add); // race condition!
// โ
Collect into thread-safe structure
List<String> result = list.parallelStream().filter(...).collect(Collectors.toList());
11a. Iteration Paradigms: For Loops vs. Streams vs. Parallel Streamsโ
Choosing between traditional loops, sequential streams, and parallel streams requires balancing syntax readability, control flow demands, and hardware resource constraints.
1. Architectural and Behavioral Matrixโ
| Aspect | Traditional for / for-each Loop | Sequential Stream | Parallel Stream |
|---|---|---|---|
| Programming Paradigm | Imperative: Focuses on how to do it (explicit state changes). | Declarative: Focuses on what to do (functional pipelines). | Declarative Concurrent: Focuses on what to do, executed across threads. |
| Iteration Mechanism | External: Client code controls how elements are pulled and navigated. | Internal: The framework manages traversal behind the scenes. | Internal: Traversal is handled by the framework and chunked across threads. |
| Control Flow | Supports break, continue, and early return. | Bypasses break/continue (uses filter/limit instead); early returns are restricted. | Cannot use break/continue; early returns are non-trivial (uses short-circuiting). |
| State Mutability | Encourages mutating shared variables. | Promotes immutable data and stateless operations. | Requires stateless, thread-safe, and independent operations. |
| Memory Allocation | Zero framework-level allocations; highly memory efficient. | Instantiates intermediate pipeline descriptors and sinks (minor heap tax). | High overhead due to Fork/Join tasks, thread local structures, and merging. |
| Debuggability | Easy: Stack traces map 1:1 with execution line; local variables are inspectable. | Complex: Lazy execution hides actual execution lines in massive stack traces. | Difficult: Threads interleave execution; debugger tracing is non-linear. |
2. Functional Equivalence (Coding Example)โ
Consider filtering a list of integers to keep only even numbers, multiplying them by two, and collecting them into a list:
Imperative (Traditional Loop)โ
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> result = new ArrayList<>();
for (Integer num : numbers) {
if (num % 2 == 0) {
result.add(num * 2);
}
}
Declarative (Sequential Stream)โ
List<Integer> result = numbers.stream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.collect(Collectors.toList());
Declarative (Parallel Stream)โ
List<Integer> result = numbers.parallelStream()
.filter(num -> num % 2 == 0)
.map(num -> num * 2)
.collect(Collectors.toList());
3. Under the Hood: Internal Mechanicsโ
A. Traditional Loopsโ
At the bytecode level, a traditional loop is compiled directly into local variable read/write instructions and simple conditional jump instructions (such as goto and if_icmpge). The JVM executes this loop sequentially within a single thread's stack frame. Because there is no object wrapper layer, the JIT compiler can aggressively optimize this code using loop unrolling and escape analysis.
B. Sequential Streamsโ
When a sequential stream pipeline is executed, Java performs two distinct steps:
- Pipeline Construction: It wraps the source collection in a linked chain of
AbstractPipelineobjects, with each intermediate operation (e.g.,filter,map) representing a node. - Terminal Evaluation: When the terminal operation (e.g.,
collect) is invoked, the stream engine constructs a nested chain ofSinkobjects from the last node back to the first. It then uses the source collection'sSpliteratorto push elements down theSinkchain one by one. The data flows sequentially, processing each element through the entire pipeline before moving to the next.
C. Parallel Streamsโ
Parallel streams build the same pipeline, but instead of traversing sequentially, they partition the data and run it concurrently:
- Splitting the Source: The terminal operation queries the source
Spliteratorand recursively invokestrySplit()to split the collection into balanced sub-chunks. - Task Scheduling: It wraps these sub-chunks in
ForkJoinTaskobjects and submits them to the JVM's sharedForkJoinPool.commonPool(). - Execution: Worker threads run the task chunks in parallel using work-stealing queues.
- Reduction/Merging: Once threads complete their tasks, they merge their results back using Combiner functions (specified by the Collector or reduction operator).
[Source Collection]
|
(Spliterator.trySplit())
|
+---------------+---------------+
| |
[Sub-Chunk 1] [Sub-Chunk 2]
| |
(ForkJoinTask 1) (ForkJoinTask 2)
| |
[Worker Thread 1] [Worker Thread 2]
| |
[Result 1] [Result 2]
+---------------+---------------+
|
(Combiner Merge)
|
[Final Result]
4. Performance Tradeoffs and Production Trapsโ
Goetz's N \times Q Rule of Thumbโ
To determine if parallel streams are faster than sequential ones, use Brian Goetz's formula:
\text{Performance Gain} \propto N \times Q
Where:
N: The number of data elements.Q: The computational cost to process a single element (e.g. arithmetic vs. string parsing).
If N \times Q > 10,000, parallelization overhead (splitting, thread scheduling, and merging) is usually offset by concurrent CPU execution. If Q is extremely low (such as simple addition), N must be millions of elements to justify a parallel stream.
Data Source Splittability (Spliterator Efficiency)โ
Parallel stream efficiency is highly dependent on how cheaply the source collection can be divided into equal parts.
- ArrayList, Arrays, and
IntStream.range()(Excellent): Split inO(1)time by adjusting array index boundaries. They areSIZEDandSUBSIZED, allowing the scheduler to divide tasks perfectly. - HashSet and HashMap (Good): Split reasonably well based on internal hash bucket structures, but hash collisions can create unequal chunk sizes.
- LinkedList and
BufferedReader.lines()(Poor): Splitting requires traversing the list nodes, which is anO(N)operation. This makes parallel streams on LinkedLists slower than sequential iteration.
The Shared ForkJoinPool Starvation Hazardโ
By default, all parallel streams in a running JVM share a single, static thread pool: ForkJoinPool.commonPool().
- The Trap: If a developer runs blocking operations (like database calls, HTTP requests, or Thread.sleep) inside a parallel stream, those threads become blocked:
// โ ๏ธ TRAP: Blocks shared JVM threads!listOfOrders.parallelStream().map(order -> callExternalPaymentGateway(order)) // Blocking I/O.collect(Collectors.toList());
- The Impact: Because the pool is shared, blocking these threads starves every other parallel stream running in the application (even unrelated background tasks).
- The Solution: For blocking operations, bypass parallel streams and use an explicit custom thread pool, or wrap the parallel stream in a custom
ForkJoinPoolsubmission:// Custom thread pool executionForkJoinPool customPool = new ForkJoinPool(8);List<Result> results = customPool.submit(() ->listOfOrders.parallelStream().map(this::callExternalPaymentGateway).toList()).get();
12. Scoped Values (Java 21+ โ Preview)โ
ScopedValue is the modern replacement for ThreadLocal โ designed for virtual threads and structured concurrency where thread pool reuse breaks ThreadLocal semantics.
// Declare a scoped value (like a typed "dynamic variable")
static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
// Set and run โ value lives only within the scope (not leaked to thread pool)
ScopedValue.where(CURRENT_USER, authenticatedUser).run(() -> {
processRequest(); // CURRENT_USER is readable anywhere in this call stack
});
// Read anywhere inside the scope
public void processRequest() {
User user = CURRENT_USER.get(); // Safe, no ThreadLocal memory leak risk
}
ScopedValue vs ThreadLocalโ
ThreadLocal | ScopedValue | |
|---|---|---|
| Scope | Entire thread lifetime | Bounded to a code scope |
| Virtual thread safe | โ (can leak across reused threads) | โ |
| Memory leak risk | High (must call remove()) | None (automatically scoped) |
| Mutable | Yes | Immutable (rebind = new scope) |
| Structured concurrency | Poor fit | First-class fit |
Advanced Editorial Pass: Feature Adoption with Migration Disciplineโ
Decision Frameworkโ
- Adopt features when they reduce defect rate or cognitive load, not for novelty.
- Sequence upgrades by platform compatibility, library ecosystem readiness, and team fluency.
- Protect rollout with compatibility tests across runtime and build toolchain.
Adoption Risksโ
- Mixed-language style across modules increases maintenance friction.
- Incomplete migration strategies create hidden behavioral inconsistencies.
- Feature use outpaces debugging and observability competence.
Migration Heuristicsโ
- Define approved feature subsets per Java version and team maturity.
- Enforce consistent style through reviews and static analysis.
- Pair language upgrades with targeted knowledge-sharing and incident drills.
Compare Nextโ
Interview Questions (Senior Level)โ
- How do you prioritize Java language feature adoption across teams with mixed service maturity and SLAs?
- What migration plan would you propose from Java 11 to 21 for a large microservice estate?
- When do virtual threads provide meaningful gains, and where can they hurt system behavior?
- How do records, sealed classes, and pattern matching improve domain modeling in real codebases?
Short answer guide:
- Adopt by business value, tooling readiness, and operational safety.
- Use phased upgrades, compatibility test matrices, and runtime observability gates.
- Apply virtual threads for blocking I/O concurrency, not CPU-bound work.
- Use modern type features to reduce boilerplate and illegal state space.