Sitemap

Java17 vs Java8

18 min readMar 15, 2026

--

Java 17 vs Java 8: The Complete Guide to Modern Java Features

A comprehensive guide covering everything new from Java 8 to Java 17 with code examples

If you’re still on Java 8 and wondering whether to upgrade, or you’re preparing for interviews, this guide covers every significant feature introduced between Java 8 and Java 17 — with side-by-side code comparisons.

🚀 1. New Language Features (Cleaner Code, Less Boilerplate)

1.1 Records (Preview in Java 14, Standardized in Java 16)

What is it?
A new type of class designed to act as a transparent data carrier. It automatically generates constructors, accessors (like name() and age()), equals(), hashCode(), and toString() under the hood.

The Problem (Java 8):

// JAVA 8: Creating a simple data class requires LOTS of boilerplate
public class Person {
private final String name;
private final int age;
private final String email;
    // Constructor
public Person(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// Getters
public String getName() { return name; }
public int getAge() { return age; }
public String getEmail() { return email; }
// equals()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name) &&
Objects.equals(email, person.email);
}
// hashCode()
@Override
public int hashCode() {
return Objects.hash(name, age, email);
}
// toString()
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + ", email='" + email + "'}";
}
}
// That's 40+ lines for a simple data holder!

The Solution (Java 17):

// JAVA 17: One line does it all!
public record Person(String name, int age, String email) {}
// That's it! You automatically get:
// ✅ Constructor
// ✅ Accessors: name(), age(), email()
// ✅ equals()
// ✅ hashCode()
// ✅ toString()
// ✅ Shallow Immutability (fields are final, but beware of passing mutable objects like ArrayLists!)

Records with Validation:

public record Employee(String id, String name, double salary) {

// Compact constructor for validation
public Employee {
if (salary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
name = name.toUpperCase(); // Can modify parameters
}

// You can add custom methods
public String getFormattedSalary() {
return String.format("$%.2f", salary);
}
}

When to use Records:

  • DTOs (Data Transfer Objects)
  • API Request/Response objects
  • Value Objects
  • Immutable configurations

1.2 Text Blocks (Java 13–15)

What is it?
Multi-line string literals using triple quotes ("""). This makes writing JSON payloads, SQL queries, or HTML directly inside your Java code incredibly clean.

The Problem (Java 8):

// JAVA 8: String concatenation nightmare
String json = "{\n" +
" \"name\": \"John\",\n" +
" \"age\": 30,\n" +
" \"email\": \"john@example.com\"\n" +
"}";
String sql = "SELECT e.id, e.name, e.salary " +
"FROM employees e " +
"WHERE e.department = 'Engineering' " +
"AND e.salary > 50000 " +
"ORDER BY e.name";
String html = "<html>\n" +
" <body>\n" +
" <h1>Hello World</h1>\n" +
" </body>\n" +
"</html>";

The Solution (Java 17):

// JAVA 17: Text Blocks - Clean and readable!
String json = """
{
"name": "John",
"age": 30,
"email": "john@example.com"
}
""";
String sql = """
SELECT e.id, e.name, e.salary
FROM employees e
WHERE e.department = 'Engineering'
AND e.salary > 50000
ORDER BY e.name
""";
String html = """
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
""";

With String Interpolation:

String name = "John";
String appName = "MyApp";
String greeting = """
Hello %s,

Welcome to %s!
We're excited to have you on board.

Best regards,
The Team
""".formatted(name, appName);

1.3 Switch Expressions (Java 12–14)

What is it?
The traditional switch statement got a major upgrade. You can use arrow syntax (->) to eliminate fall-through (no more break statements), and the switch block can now directly return a value.

The Problem (Java 8):

// JAVA 8: Verbose switch statement
public String getDayType(String day) {
String type;
switch (day) {
case "MONDAY":
case "TUESDAY":
case "WEDNESDAY":
case "THURSDAY":
case "FRIDAY":
type = "Weekday";
break; // Easy to forget!
case "SATURDAY":
case "SUNDAY":
type = "Weekend";
break;
default:
type = "Unknown";
break;
}
return type;
}

The Solution (Java 17):

// JAVA 17: Switch Expression - returns a value directly!
public String getDayType(String day) {
return switch (day) {
case "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY" -> "Weekday";
case "SATURDAY", "SUNDAY" -> "Weekend";
default -> "Unknown";
};
}

With Complex Logic (yield):

public int calculate(String operation, int a, int b) {
return switch (operation) {
case "add" -> a + b;
case "subtract" -> a - b;
case "multiply" -> a * b;
case "divide" -> {
if (b == 0) {
System.out.println("Cannot divide by zero");
yield 0; // Use 'yield' for multi-line blocks
}
yield a / b;
}
default -> throw new IllegalArgumentException("Unknown operation");
};
}

With Enums (Exhaustiveness Check):

enum Status { PENDING, APPROVED, REJECTED }
public String getStatusMessage(Status status) {
return switch (status) {
case PENDING -> "Your request is being processed";
case APPROVED -> "Congratulations! Approved!";
case REJECTED -> "Sorry, rejected";
// No default needed - compiler knows all enum values!
};
}

1.4 Pattern Matching for instanceof (Java 14–16)

What is it?
You no longer need to explicitly cast an object immediately after an instanceof check. If the check passes, the object is automatically cast and bound to a new variable in the same line.

The Problem (Java 8):

// JAVA 8: Verbose and error-prone
public void process(Object obj) {
if (obj instanceof String) {
String str = (String) obj; // Manual cast required
System.out.println(str.toUpperCase());
} else if (obj instanceof Integer) {
Integer num = (Integer) obj; // Manual cast again
System.out.println(num * 2);
} else if (obj instanceof List) {
List<?> list = (List<?>) obj; // And again...
System.out.println(list.size());
}
}

The Solution (Java 17):

// JAVA 17: Clean and type-safe
public void process(Object obj) {
if (obj instanceof String str) { // Check + cast + assign in one!
System.out.println(str.toUpperCase());
} else if (obj instanceof Integer num) {
System.out.println(num * 2);
} else if (obj instanceof List<?> list) {
System.out.println(list.size());
}
}

With Logical Operators:

// Pattern variable works with && 
public void validateString(Object obj) {
if (obj instanceof String s && s.length() > 5) {
System.out.println("Long string: " + s);
}
}
// Negation pattern
public void processNonString(Object obj) {
if (!(obj instanceof String s)) {
System.out.println("Not a string!");
return;
}
// s is in scope here!
System.out.println("String: " + s.toUpperCase());
}

1.5 Sealed Classes (Java 15–17)

What is it?
You can now explicitly control your inheritance hierarchies. By declaring a class as sealed and using the permits keyword, you restrict exactly which other classes are allowed to extend it.

The Problem (Java 8):

// JAVA 8: No way to restrict which classes can extend
public abstract class Shape { }
// Anyone can extend Shape - no control!
class Circle extends Shape { }
class Square extends Shape { }
class WeirdShape extends Shape { } // Can't prevent this!

The Solution (Java 17):

// JAVA 17: Explicit control over class hierarchy
public sealed class Shape
permits Circle, Rectangle, Triangle {
// Only Circle, Rectangle, Triangle can extend Shape
}
// Subclasses must be: final, sealed, or non-sealed
public final class Circle extends Shape {
private final double radius;
// final = cannot be extended further
}
public sealed class Rectangle extends Shape
permits Square {
// Rectangle can only be extended by Square
}
public final class Square extends Rectangle {
// Square is final
}
public non-sealed class Triangle extends Shape {
// non-sealed = anyone can extend Triangle (opt-out)
}

Sealed Interfaces:

public sealed interface Animal 
permits Dog, Cat, Bird {
}
public final class Dog implements Animal { }
public final class Cat implements Animal { }
public record Bird(String species) implements Animal { } // Records work too!

Why Use Sealed Classes?

  • Domain Modeling: Define a fixed set of subtypes (like payment methods: CreditCard, DebitCard, PayPal)
  • Exhaustive Switch: Compiler knows all subtypes, no need for default case
  • Security: Prevent unauthorized extensions of your classes

1.6 Local Variable Type Inference (var) (Java 10)

What is it?
You can use the var keyword to let the compiler infer the type of a local variable. This reduces verbosity while maintaining type safety.

The Problem (Java 8):

// JAVA 8: Explicit types everywhere
HashMap<String, List<Employee>> employeesByDepartment = new HashMap<>();
BufferedReader reader = new BufferedReader(new FileReader("file.txt"));
List<String> filteredNames = names.stream()
.filter(n -> n.startsWith("A"))
.collect(Collectors.toList());

The Solution (Java 17):

// JAVA 10+: var - let the compiler figure it out
var employeesByDepartment = new HashMap<String, List<Employee>>();
var reader = new BufferedReader(new FileReader("file.txt"));
var filteredNames = names.stream()
.filter(n -> n.startsWith("A"))
.collect(Collectors.toList());

Great Use Cases:

// Factory methods - type is obvious
var list = List.of(1, 2, 3); // List<Integer>
var map = Map.of("key", "value"); // Map<String, String>
var set = Set.of("a", "b", "c"); // Set<String>
var optional = Optional.of("hello"); // Optional<String>
// In loops - reduces noise
for (var entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// Try-with-resources
try (var reader = new BufferedReader(new FileReader("file.txt"))) {
// ...
}

When NOT to Use var:

// ❌ BAD - unclear type
var result = someMethod(); // What type is this?
// ✅ GOOD - type is obvious
var users = new ArrayList<User>();
var count = 10;
var name = "John";
// ❌ CANNOT use without initializer
var x; // Compilation error!
// ❌ CANNOT use with null
var y = null; // Compilation error!
// ❌ CANNOT use for fields or parameters
private var field = "hello"; // Error!
public void method(var param) {} // Error!

⚡ 2. Performance and JVM Enhancements

2.1 Helpful NullPointerExceptions (Java 14)

What is it?
Instead of just pointing to the line where a crash happened, the JVM now explicitly tells you which specific method call or variable was null in a chained statement.

The Problem (Java 8):

String city = employee.getAddress().getCity().toUpperCase();
// Java 8 Exception:
// Exception in thread "main" java.lang.NullPointerException
// at MyClass.main(MyClass.java:15)
//
// Which one is null? employee? getAddress()? getCity()? 🤷

The Solution (Java 17):

String city = employee.getAddress().getCity().toUpperCase();
// Java 14+ Exception:
// Exception in thread "main" java.lang.NullPointerException:
// Cannot invoke "Address.getCity()" because the return value of
// "Employee.getAddress()" is null
// at MyClass.main(MyClass.java:15)
//
// Now you know EXACTLY what's null! ✅

More Examples:

// Array access
String[] names = null;
int length = names.length;
// NPE: Cannot read the array length because "names" is null
// Map access
Map<String, User> users = null;
User user = users.get("john");
// NPE: Cannot invoke "Map.get(Object)" because "users" is null

2.2 Garbage Collection Upgrades

Java 8 used the Parallel GC by default. Java 17 uses G1GC (Garbage-First) as the default, which dramatically reduces pause times. Java 17 also includes ZGC, designed for ultra-low-latency applications.

┌─────────────────────────────────────────────────────────────────────────────┐
│ GARBAGE COLLECTOR COMPARISON │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ JAVA 8 DEFAULT: Parallel GC │
│ ───────────────────────────── │
│ • Optimized for THROUGHPUT (maximize work done) │
│ • Can have long pause times (stop-the-world) │
│ • Good for batch processing, not for interactive apps │
│ │
│ JAVA 9+ DEFAULT: G1GC (Garbage-First) │
│ ────────────────────────────────────── │
│ • Balanced between throughput and latency │
│ • Predictable pause times (target: < 200ms) │
│ • Divides heap into regions, collects garbage-first regions │
│ • Better for large heaps (> 4GB) │
│ │
│ JAVA 11+: ZGC (Z Garbage Collector) │
│ ─────────────────────────────────── │
│ • Ultra-low latency (pause times < 10ms) │
│ • Scales to multi-terabyte heaps │
│ • Concurrent - most work done while app runs │
│ • Enable with: -XX:+UseZGC │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

How to Select a Garbage Collector:

# Use G1GC (default in Java 9+)
java -XX:+UseG1GC -jar myapp.jar
# Use ZGC (ultra-low latency)
java -XX:+UseZGC -jar myapp.jar
# Use Shenandoah GC (low pause times)
java -XX:+UseShenandoahGC -jar myapp.jar
# Use Parallel GC (Java 8 default, throughput-focused)
java -XX:+UseParallelGC -jar myapp.jar

When to Use Which GC:

GC TypeBest ForParallel GCBatch jobs, scientific computing (throughput-focused, latency not critical)G1GCMost applications — web servers, microservices (balanced)ZGCLow-latency critical apps — trading systems, gaming servers, large heaps (100GB+)ShenandoahSimilar to ZGC, alternative option

2.3 Free Performance Boost

Thanks to years of JIT compiler optimizations and better memory management, running an older Java 8 application on the Java 17 JVM will generally result in faster execution and lower memory usage without changing a single line of code.

What You Get:

ImprovementDescriptionJIT CompilerBetter inlining decisions, improved escape analysis, more aggressive optimizationsCompact Strings (Java 9)Strings use ~50% less memory (Latin-1 encoding for ASCII)Class LoadingFaster startup with Application Class-Data Sharing (AppCDS)Memory ManagementReduced GC overhead, better object layout

Benchmark Example (typical results):

Same application, same code:
Java 8:  100 requests/sec, 512MB heap, 200ms avg latency
Java 17: 130 requests/sec, 400MB heap, 150ms avg latency
That's ~30% improvement WITHOUT changing any code!

🔧 3. API Updates

3.1 Modern HTTP Client (Java 11)

What is it?
The clunky and outdated HttpURLConnection was replaced with a brand new, highly efficient HttpClient API. It uses a builder pattern, natively supports HTTP/2, and handles asynchronous calls smoothly.

The Problem (Java 8):

// JAVA 8: HttpURLConnection - very verbose and painful
URL url = new URL("https://api.example.com/users");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
int responseCode = conn.getResponseCode();
BufferedReader in = new BufferedReader(
new InputStreamReader(conn.getInputStream()));
String inputLine;
StringBuilder response = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
String body = response.toString();

The Solution (Java 17):

// JAVA 11+: Modern HttpClient - clean and powerful
HttpClient client = HttpClient.newHttpClient();
// Simple GET request
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Accept", "application/json")
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());

Asynchronous Requests:

// Non-blocking async request
client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body)
.thenAccept(System.out::println)
.join();

POST Request with JSON:

HttpRequest postRequest = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString("""
{
"name": "John Doe",
"email": "john@example.com"
}
"""))
.build();
HttpResponse<String> response = client.send(
postRequest,
HttpResponse.BodyHandlers.ofString()
);

3.2 Stream API Additions (Java 9–16)

Several quality-of-life methods were added to the Stream API.

Get Yunus Iqbal Siddique’s stories in your inbox

Join Medium for free to get updates from this writer.

toList() — Java 16:

// JAVA 8 - verbose
List<String> list = stream.collect(Collectors.toList());
// JAVA 16+ - clean and simple!
List<String> list = stream.toList();
// Note: Returns an unmodifiable list

takeWhile() and dropWhile() — Java 9:

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 1, 2);
// takeWhile - take elements WHILE condition is true
List<Integer> taken = numbers.stream()
.takeWhile(n -> n < 4)
.toList();
// Result: [1, 2, 3]
// dropWhile - skip elements WHILE condition is true
List<Integer> dropped = numbers.stream()
.dropWhile(n -> n < 4)
.toList();
// Result: [4, 5, 6, 1, 2]

iterate() with termination — Java 9:

// JAVA 8 - need limit() to terminate
Stream.iterate(1, n -> n * 2)
.limit(10)
.forEach(System.out::println);
// JAVA 9+ - built-in termination condition
Stream.iterate(1, n -> n < 1000, n -> n * 2)
.forEach(System.out::println);

ofNullable() — Java 9:

String nullableValue = null;
// JAVA 8 - would throw NPE
// Stream.of(nullableValue).forEach(System.out::println); // NPE!
// JAVA 9+ - handles null gracefully
Stream.ofNullable(nullableValue)
.forEach(System.out::println); // Prints nothing, no NPE

3.3 Enhanced Optional (Java 9–11)

ifPresentOrElse() — Java 9:

Optional<String> optional = Optional.of("Hello");
// JAVA 8 - awkward
if (optional.isPresent()) {
System.out.println("Found: " + optional.get());
} else {
System.out.println("Not found");
}
// JAVA 9+ - clean!
optional.ifPresentOrElse(
value -> System.out.println("Found: " + value),
() -> System.out.println("Not found")
);

or() — Java 9:

// Lazy alternative - only computed if original is empty
Optional<String> result = primaryOptional
.or(() -> getBackupOptional())
.or(() -> Optional.of("default"));

stream() — Java 9:

// Convert Optional to Stream (0 or 1 element)
Optional<String> opt1 = Optional.of("Hello");
Optional<String> opt2 = Optional.empty();
Optional<String> opt3 = Optional.of("World");
// Combine multiple optionals, filtering out empties
List<String> values = Stream.of(opt1, opt2, opt3)
.flatMap(Optional::stream)
.toList();
// Result: ["Hello", "World"]

isEmpty() — Java 11:

// More readable than !isPresent()
if (optional.isEmpty()) {
System.out.println("No value present");
}

orElseThrow() without args — Java 10:

// Throws NoSuchElementException if empty
String value = optional.orElseThrow();

🏗️ 4. Architectural & Security Enhancements

Beyond syntax improvements, Java 17 completely overhauled the JVM’s architecture, security, and internal APIs. These are the heavy-hitting structural changes for enterprise systems.

4.1 Strong Encapsulation & Module System (Project Jigsaw) (Java 9–17)

What is it?
Java 9 introduced the Java Platform Module System (JPMS), but Java 17 is the release that finally enforced it strictly by encapsulating JDK internals (removing the --illegal-access flag).

The Problem (Java 8):

// JAVA 8: Developers could hack into internal JDK APIs using reflection
// This was commonly used but dangerous and unsupported
import sun.misc.Unsafe;  // Internal API - worked in Java 8Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true); // Reflection hack to access private fields
Unsafe unsafe = (Unsafe) f.get(null);
// Libraries like Spring, Hibernate heavily used these hacks
// Problem: These could break with any JDK update!

The Solution (Java 17):

// JAVA 9+: Define explicit module boundaries in module-info.java
// File: module-info.java (in src/main/java/)
module com.myapp.core {
// What this module needs
requires java.sql;
requires java.logging;
requires com.myapp.utils;

// What this module exposes to others
exports com.myapp.core.api;
exports com.myapp.core.model;

// Internal packages stay hidden (not exported)
// com.myapp.core.internal is NOT accessible to other modules

// Allow specific modules to use reflection on us
opens com.myapp.core.model to com.fasterxml.jackson.databind;
}

Practical Example — Modular Application:

// module-info.java for a microservice
module com.payment.service {
// Dependencies
requires java.net.http; // HTTP Client
requires java.sql; // Database
requires spring.boot; // Spring Boot
requires spring.web; // Spring Web

// Public API
exports com.payment.api;
exports com.payment.dto;

// Allow Spring to reflect on our classes
opens com.payment to spring.core, spring.beans;
opens com.payment.config to spring.context;
}

Compile-Time Enforcement:

# Java 17 will fail at compile time if dependencies aren't declared
javac --module-source-path src -d out $(find src -name "*.java")
# Error: package com.internal.api is not visible
# (package com.internal.api is declared in module com.other,
# which does not export it)

Why This Matters:

  • Security: Internal JDK APIs are locked down — no more sun.misc.Unsafe hacks
  • Maintainability: Clear module boundaries prevent spaghetti dependencies
  • Reliability: Breaking changes in internal APIs won’t break your code
  • Microservices: Module boundaries enforce strict dependency rules at compile-time

4.2 Immutable Collection Factory Methods (Java 9)

What is it?
List.of(), Set.of(), and Map.of() create highly optimized, truly immutable collections that reject nulls by default.

The Problem (Java 8):

// JAVA 8: Creating immutable collections was verbose and error-prone
// Creating an "unmodifiable" list
List<String> list = Collections.unmodifiableList(
Arrays.asList("a", "b", "c")
);
// Creating an "unmodifiable" map - even worse!
Map<String, Integer> map = Collections.unmodifiableMap(
new HashMap<String, Integer>() {{
put("one", 1);
put("two", 2);
put("three", 3);
}}
);
// Problems:
// 1. The underlying collection can still be modified if you have a reference
// 2. Allows nulls (can cause NPEs later)
// 3. Not memory-optimized
// 4. Verbose and ugly syntax

The Solution (Java 17):

// JAVA 9+: Clean, optimized, truly immutable collections
// Immutable List
List<String> list = List.of("a", "b", "c");
// Immutable Set
Set<String> set = Set.of("x", "y", "z");
// Immutable Map (up to 10 key-value pairs)
Map<String, Integer> map = Map.of(
"one", 1,
"two", 2,
"three", 3
);
// For larger maps, use Map.ofEntries()
Map<String, Integer> largeMap = Map.ofEntries(
Map.entry("one", 1),
Map.entry("two", 2),
Map.entry("three", 3),
Map.entry("four", 4),
Map.entry("five", 5)
// ... can have many more
);

Key Characteristics:

// ❌ Nulls are rejected immediately
List<String> list = List.of("a", null, "c");
// Throws NullPointerException at creation time!
// ❌ Modifications are rejected
List<String> list = List.of("a", "b");
list.add("c"); // UnsupportedOperationException!
// ❌ Duplicates in Sets are rejected
Set<String> set = Set.of("a", "a", "b");
// Throws IllegalArgumentException!
// ✅ Thread-safe by design - no synchronization needed
// ✅ Memory-optimized internal implementation
// ✅ Can be safely shared across threads

Copying Collections (Java 10):

// Create immutable copy of any collection
List<String> mutableList = new ArrayList<>(List.of("a", "b", "c"));
mutableList.add("d"); // Works - mutable
// Create an immutable snapshot
List<String> immutableCopy = List.copyOf(mutableList);
// immutableCopy is ["a", "b", "c", "d"] and cannot be changed
// Original can still be modified
mutableList.add("e"); // immutableCopy is NOT affected

Architectural Impact — Thread-Safe Caching:

// Use case: Configuration or reference data caching
public class AppConfig {

// Thread-safe without synchronization!
private static final Map<String, String> SETTINGS = Map.of(
"api.timeout", "30000",
"api.retries", "3",
"cache.ttl", "3600"
);

private static final Set<String> VALID_REGIONS = Set.of(
"us-east-1", "us-west-2", "eu-west-1", "ap-south-1"
);

public static String getSetting(String key) {
return SETTINGS.get(key); // No locking needed!
}

public static boolean isValidRegion(String region) {
return VALID_REGIONS.contains(region); // Thread-safe!
}
}

4.3 Context-Specific Deserialization Filters (Java 17)

What is it?
A major security enhancement that allows you to create dynamic, context-specific filters to prevent deserialization attacks using ObjectInputFilter.

The Problem (Deserialization Attacks):

// THE DANGER: Deserialization attacks are one of the most dangerous
// vulnerabilities in Java history
// Attacker can craft malicious serialized objects that execute code
// when deserialized by your application
// Java 8 - No built-in protection
ObjectInputStream ois = new ObjectInputStream(untrustedInput);
Object obj = ois.readObject(); // 💀 Could execute arbitrary code!
// Famous vulnerabilities: Apache Commons Collections, Spring Framework
// Attackers could gain complete control of servers through this!

The Solution (Java 17):

// JAVA 17: Context-specific deserialization filters
import java.io.ObjectInputFilter;// Create a filter that only allows specific classes
ObjectInputFilter safeFilter = ObjectInputFilter.Config.createFilter(
"com.myapp.dto.*;java.lang.*;java.util.*;!*"
// Allow: com.myapp.dto.*, java.lang.*, java.util.*
// Deny: everything else (the !* at the end)
);
// Apply filter to ObjectInputStream
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(safeFilter);
// Now only safe classes can be deserialized
Object obj = ois.readObject(); // Protected!

Advanced Filter with Limits:

// Create a filter with size and depth limits
ObjectInputFilter strictFilter = ObjectInputFilter.Config.createFilter(
"maxdepth=5;" + // Max object graph depth
"maxrefs=1000;" + // Max object references
"maxbytes=100000;" + // Max bytes to read
"maxarray=10000;" + // Max array size
"com.myapp.model.*;" + // Allow our models
"java.util.ArrayList;" + // Allow ArrayList
"java.util.HashMap;" + // Allow HashMap
"!*" // Deny everything else
);

Programmatic Filter:

// Create a custom filter with complex logic
ObjectInputFilter customFilter = filterInfo -> {
Class<?> clazz = filterInfo.serialClass();

if (clazz == null) {
return ObjectInputFilter.Status.UNDECIDED;
}

// Allow only classes in our package
if (clazz.getPackageName().startsWith("com.myapp")) {
return ObjectInputFilter.Status.ALLOWED;
}

// Allow standard Java classes
if (clazz.getPackageName().startsWith("java.lang") ||
clazz.getPackageName().startsWith("java.util")) {
return ObjectInputFilter.Status.ALLOWED;
}

// Check object graph limits
if (filterInfo.depth() > 10) {
return ObjectInputFilter.Status.REJECTED;
}

if (filterInfo.references() > 10000) {
return ObjectInputFilter.Status.REJECTED;
}

// Reject unknown classes
return ObjectInputFilter.Status.REJECTED;
};
ois.setObjectInputFilter(customFilter);

Global Filter (JVM-wide protection):

// Set a global filter for the entire JVM
// Usually done at application startup
ObjectInputFilter globalFilter = ObjectInputFilter.Config.createFilter(
"!sun.reflect.*;!java.lang.reflect.*;!java.rmi.*;" +
"com.myapp.**;java.base/**;!*"
);
ObjectInputFilter.Config.setSerialFilter(globalFilter);// Now ALL ObjectInputStream instances have this protection!

Why This Matters:

  • Security: Prevents remote code execution through deserialization
  • Defense in Depth: Even if attacker bypasses other security, this stops them
  • Production Safety: Essential for any system that deserializes untrusted data
  • Compliance: Required for security-conscious enterprises

4.4 Enhanced Pseudo-Random Number Generators (Java 17)

What is it?
Java 17 introduced RandomGenerator and RandomGeneratorFactory interfaces to unify random number generation with a pluggable architecture and new efficient algorithms.

The Problem (Java 8):

// JAVA 8: Hardcoded random implementations
// For general use
Random random = new Random();
int number = random.nextInt(100);
// For cryptographic security
SecureRandom secureRandom = new SecureRandom();
byte[] token = new byte[32];
secureRandom.nextBytes(token);
// For parallel streams (thread-local)
ThreadLocalRandom tlr = ThreadLocalRandom.current();
int parallel = tlr.nextInt(100);
// Problems:
// 1. Can't swap algorithms without code changes
// 2. Hard to test (can't inject different implementations)
// 3. Limited algorithm choices
// 4. No unified API

The Solution (Java 17):

// JAVA 17: Unified, pluggable random API
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
// Use the unified interface
RandomGenerator random = RandomGenerator.getDefault();
int number = random.nextInt(100);
double decimal = random.nextDouble();
long bigNumber = random.nextLong();
// Get a specific algorithm
RandomGenerator xoroshiro = RandomGenerator.of("Xoroshiro128PlusPlus");
RandomGenerator l64x128 = RandomGenerator.of("L64X128MixRandom");
// For cryptographic purposes
RandomGenerator secure = RandomGenerator.of("SecureRandom");

Discover Available Algorithms:

// List all available random algorithms
RandomGeneratorFactory.all()
.map(factory -> factory.name() + " - " + factory.group())
.sorted()
.forEach(System.out::println);
// Output:
// L128X1024MixRandom - LXM
// L128X128MixRandom - LXM
// L128X256MixRandom - LXM
// L32X64MixRandom - LXM
// L64X1024MixRandom - LXM
// L64X128MixRandom - LXM
// L64X256MixRandom - LXM
// Random - Legacy
// SecureRandom - Legacy
// SplittableRandom - Splittable
// Xoroshiro128PlusPlus - Xoroshiro
// Xoshiro256PlusPlus - Xoshiro

Choose Algorithm by Properties:

// Find the best algorithm for your use case
RandomGeneratorFactory<?> factory;
// Need a splittable generator for parallel streams?
factory = RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isSplittable)
.findFirst()
.orElseThrow();
// Need a jumpable generator?
factory = RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isJumpable)
.findFirst()
.orElseThrow();
// Need specific state size for reproducibility?
factory = RandomGeneratorFactory.all()
.filter(f -> f.stateBits() >= 128)
.findFirst()
.orElseThrow();
RandomGenerator generator = factory.create();

Dependency Injection Pattern:

// Now you can inject random generators for testability!
public class TokenService {
private final RandomGenerator random;

// Constructor injection - easy to test!
public TokenService(RandomGenerator random) {
this.random = random;
}

// Production: inject SecureRandom
// Testing: inject a seeded generator for reproducible tests

public String generateToken() {
byte[] bytes = new byte[32];
random.nextBytes(bytes);
return Base64.getEncoder().encodeToString(bytes);
}
}
// Production
TokenService prodService = new TokenService(
RandomGenerator.of("SecureRandom")
);
// Testing - reproducible!
TokenService testService = new TokenService(
RandomGenerator.of("L64X128MixRandom").create(12345L) // seeded
);

Parallel Stream Support:

// Generate random numbers in parallel streams efficiently
RandomGenerator.SplittableGenerator generator =
(RandomGenerator.SplittableGenerator) RandomGenerator.of("L64X128MixRandom");
// Split into independent generators for parallel processing
List<Integer> randomNumbers = generator.splits(10)
.parallel()
.map(rng -> rng.nextInt(1000))
.toList();
// Each split is statistically independent!

Why This Matters:

  • Flexibility: Swap algorithms without changing code
  • Testability: Inject mock/seeded generators for unit tests
  • Performance: New LXM family algorithms are faster than old Random
  • Security: Easy to use cryptographic generators when needed
  • Parallel: Better support for concurrent applications

📊 Quick Reference Comparison Table

Press enter or click to view image in full size

🎯 Interview Quick Tips

Language Features:

  1. Records: Use for DTOs, immutable data; auto-generates equals/hashCode/toString
  2. Sealed Classes: Control inheritance hierarchy; enables exhaustive switch
  3. Pattern Matching: instanceof String s combines check + cast
  4. Text Blocks: Triple quotes for JSON/SQL/HTML, use .formatted() for interpolation
  5. Switch Expressions: Returns values, no break needed, use yield for complex logic
  6. var: Local variables only, needs initializer, type inferred at compile time

Performance & JVM:

  1. ZGC: Sub-10ms pauses, scales to terabytes, enable with -XX:+UseZGC
  2. Helpful NPE: Shows exact null location in chained calls

API Updates:

  1. Stream.toList(): Returns unmodifiable list, cleaner than collect()
  2. HttpClient: Native HTTP/2, async support, builder pattern

Architecture & Security:

  1. JPMS Modules: module-info.java, requires, exports, opens keywords
  2. List.of()/Map.of(): Immutable, null-rejecting, thread-safe, memory-optimized
  3. ObjectInputFilter: Prevent deserialization attacks with allowlists
  4. RandomGenerator: Pluggable interface, use RandomGenerator.of("algorithm")

📚 LTS Versions

VersionTypeReleasedSupport UntilJava 8LTS2014Dec 2030Java 11LTS2018Sep 2026Java 17LTS2021Sep 2029Java 21LTS2023Sep 2031

Recommendation: For production, stick with LTS versions.

🚀 Getting Started

# Clone the project
git clone <repo-url>
cd java-17-features-demo
# Build
mvn compile
# Run all demos
mvn exec:java -Dexec.mainClass="com.java17demo.Java17FeaturesDemoApplication"

Each package contains working examples you can run individually.

Happy Coding! 🎉

If this helped you, give it a ⭐ and share with others preparing for Java interviews or considering an upgrade from Java 8!

--

--