Part 6 of 6 | The final installment in our comprehensive Java 17 features series
This is Part 6 (final) of "Java 17 Features Every Senior Developer Should Know" - your complete desktop reference for all 6 modern Java features spanning Java 10-17. This guide consolidates everything from Parts 1-5, providing syntax cards, decision matrices, and real-world patterns you can reference while working with contemporary Java code.
Complete Series Overview
We've covered 6 major Java features that fundamentally changed how developers write clean, maintainable code:
| Part | Feature | Release | Purpose |
|---|---|---|---|
| 1 | var - Type Inference | Java 10 | Eliminate verbose type declarations |
| 2 | Records - Immutable Data | Java 16 | Replace boilerplate data classes |
| 3 | Sealed Classes - Hierarchy Control | Java 17 | Enforce closed type systems |
| 4 | Pattern Matching - Type Safety | Java 16 | Atomic type checking + binding |
| 5 | Switch Expressions - Value Returns | Java 14 | Modern conditional logic |
| 6 | Text Blocks - Multi-line Strings | Java 15 | No escape sequence hell |
Why This Matters
Modern Java (10-17) brought three transformative principles:
1. Less Boilerplate
Before Java 16, creating a simple data class meant 30+ lines of repetitive code. Today:
// Java 8
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object obj) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
// Java 17
record Point(int x, int y) {}
2. More Safety
The compiler now catches entire categories of errors at compile-time:
// Sealed classes + pattern matching = exhaustive checking
sealed interface Result permits Success, Failure {}
String msg = switch (result) {
case Success(var val) -> "Got: " + val;
case Failure(var err) -> "Error: " + err;
// Compiler ensures all cases covered!
};
3. Better Readability
Code now expresses intent directly:
// Multi-line strings without escape sequences
var query = """
SELECT u.name, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.status = 'COMPLETED'
GROUP BY u.name, u.email
ORDER BY order_count DESC""";
Quick Reference: All 6 Features at a Glance
var Keyword (Java 10) - Type Inference
When: Type is obvious from the right side
var message = "Hello"; // Clear ✅
var count = 42; // Clear ✅
var users = List.of("Alice", "Bob"); // Clear ✅
var map = new HashMap<String, Integer>(); // Clear ✅
var data = fetchData(); // Unclear ❌
Key insight: Compile-time inference only - not dynamic typing. Local variables only.
Records (Java 16) - Immutable Data Carriers
When: Creating immutable data structures (DTOs, value objects, configuration)
// Basic record
record Person(String name, int age) {}
// With validation
record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException("Invalid range");
}
}
}
// With methods
record User(String email, int age) {
public User {
email = email.toLowerCase(); // Normalization
}
public boolean isAdult() {
return age >= 18;
}
public User withAge(int newAge) {
return new User(email, newAge);
}
}
// Generic records
record Pair<T, U>(T first, U second) {
public Pair<U, T> swap() {
return new Pair<>(second, first);
}
}
// Implementing interfaces
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {
public double area() { return Math.PI * radius * radius; }
}
record Rectangle(double width, double height) implements Shape {
public double area() { return width * height; }
}
Automatic: constructor, accessors (x() not getX()), equals(), hashCode(), toString()
Key insight: Immutable by default. One line replaces 30+ lines of boilerplate.
Sealed Classes (Java 17) - Controlled Inheritance
When: You need a closed type hierarchy with known subtypes
// Fixed alternatives
sealed interface Payment permits CreditCard, PayPal, BankTransfer {}
record CreditCard(String number, String expiry) implements Payment {}
record PayPal(String email) implements Payment {}
record BankTransfer(String accountNumber) implements Payment {}
// With extension point
sealed interface PaymentExtension permits CreditCard, PayPal, CustomPayment {}
non-sealed interface CustomPayment extends PaymentExtension {} // Open for extension
// Class hierarchy
sealed class Result<T> permits Success, Failure {}
final class Success<T> extends Result<T> {
private final T value;
public Success(T value) { this.value = value; }
public T getValue() { return value; }
}
final class Failure<T> extends Result<T> {
private final String error;
public Failure(String error) { this.error = error; }
public String getError() { return error; }
}
Subtypes must be: final, sealed, or non-sealed
Key insight: Compiler knows all subtypes - enables exhaustive pattern matching without default cases.
Pattern Matching (Java 16) - Type Check + Cast
When: Type checking with immediate variable use
// Basic pattern matching
if (obj instanceof String s) {
System.out.println(s.toUpperCase());
}
// With guards
if (obj instanceof String s && s.length() > 0) {
return s.toUpperCase();
}
// Simplified equals()
@Override
public boolean equals(Object obj) {
return obj instanceof Point p
&& this.x == p.x
&& this.y == p.y;
}
// Polymorphic dispatch
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (shape instanceof Rectangle r) {
return r.width() * r.height();
}
// Type hierarchies with pattern matching in switch
double area = switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h) -> w * h;
case Triangle(var b, var h) -> (b * h) / 2.0;
};
Scope rules: Variables are scoped by flow analysis:
if (obj instanceof String s && s.length() > 5) {
// 's' in scope here
}
if (!(obj instanceof String s)) {
// 's' NOT in scope
} else {
// 's' IS in scope here
}
Key insight: Eliminates manual casting. Combines type check + cast + assignment in one atomic operation.
Switch Expressions (Java 14) - Value-Returning Switch
When: Returning values from multiple conditional branches
// Basic switch expression
String dayType = switch (day) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Weekday";
case SATURDAY, SUNDAY -> "Weekend";
};
// With yield for multi-statement
int result = switch (operation) {
case ADD -> {
System.out.println("Adding numbers");
yield a + b;
}
case SUBTRACT -> {
System.out.println("Subtracting");
yield a - b;
}
case MULTIPLY -> a * b;
case DIVIDE -> a / b;
default -> throw new IllegalArgumentException("Unknown operation");
};
// Exhaustive enum matching (no default needed)
int monthDays = switch (month) {
case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
case FEBRUARY -> 28; // Ignoring leap years for simplicity
};
// Combined with sealed types for exhaustiveness
String message = switch (result) {
case Success(var value) -> "Success: " + value;
case Failure(var error) -> "Error: " + error;
// No default needed - compiler verifies all cases!
};
Arrow syntax: -> prevents fall-through (unlike traditional :)
Rules:
- Must be exhaustive (all cases covered or default)
- Use
yieldfor multi-statement branches - No
breakneeded with arrows
Key insight: Cleaner than if-else chains. Compiler enforces completeness.
Text Blocks (Java 15) - Multi-line Strings
When: Multi-line strings without escape sequence hell
// JSON without escaping quotes
String json = """
{
"name": "Alice Johnson",
"email": "alice@example.com",
"age": 30,
"active": true
}""";
// SQL queries with proper formatting
String sqlQuery = """
SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = 'ACTIVE'
AND o.created_at > NOW() - INTERVAL 30 DAY
GROUP BY u.id, u.name
HAVING COUNT(o.id) > 5
ORDER BY order_count DESC""";
// HTML without escaping
String html = """
<div class="container">
<h1>Welcome, <span class="user">%s</span></h1>
<p>Your account balance: <strong>$%.2f</strong></p>
<a href="/dashboard">View Dashboard</a>
</div>""".formatted(userName, balance);
// With indentation control
String formatted = """
Level 1
Level 2 (indented)
Level 3 (more indented)
Back to level 2
Back to level 1
""";
// Line continuation (backslash)
String poem = """
Roses are red, \\
Violets are blue, \\
Java 17 features \\
Make code shine through.""";
// Essential space (survives whitespace stripping)
String codeBlock = """
function hello() {
console.log("Hello!");
}\s
""";
// XML configuration
String config = """
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<database>
<host>localhost</host>
<port>5432</port>
<name>myapp</name>
</database>
<logging>
<level>INFO</level>
</logging>
</configuration>""";
Special escapes:
-
\s- Essential space (survives whitespace stripping) -
\- Line continuation (no newline inserted) - Standard escapes:
\n,\t,\",\\
Key insight: All line endings normalized to \n. Opening """ must be followed by newline. Closing """ position determines indent stripping.
Real-World Integration Patterns
Pattern 1: Records + var for Clean Data
var users = List.of(
new User("alice@test.com", 30),
new User("bob@test.com", 25),
new User("charlie@test.com", 35)
);
// Process with var inference
for (var user : users) {
if (user.age() >= 18) {
processAdult(user);
}
}
// Stream operations
var activeAdults = users.stream()
.filter(u -> u.age() >= 18)
.map(u -> u.email())
.toList();
Pattern 2: Sealed Classes + Pattern Matching for Type Safety
sealed interface ApiResponse<T> permits SuccessResponse, ErrorResponse {}
record SuccessResponse<T>(int status, T data) implements ApiResponse<T> {}
record ErrorResponse(int status, String message, String code) implements ApiResponse<Object> {}
// Exhaustive pattern matching guaranteed by compiler
public static <T> void handleResponse(ApiResponse<T> response) {
switch (response) {
case SuccessResponse(var status, var data) -> {
System.out.println("Success: " + data);
}
case ErrorResponse(var status, var msg, var code) -> {
System.err.println("Error [" + code + "]: " + msg);
}
// No default needed - compiler verifies all cases!
}
}
Pattern 3: Text Blocks + var for Template Building
var userName = "Alice";
var orderTotal = 149.99;
var orderId = "ORD-2025-001";
var emailTemplate = """
<html>
<body>
<h2>Order Confirmation</h2>
<p>Hello %s,</p>
<p>Your order #%s has been confirmed.</p>
<p><strong>Total: $%.2f</strong></p>
<p>Thank you for your business!</p>
</body>
</html>""".formatted(userName, orderId, orderTotal);
var sqlTemplate = """
INSERT INTO orders (user_id, total, status, created_at)
VALUES (?, ?, 'PENDING', NOW())
RETURNING id, created_at""";
Pattern 4: Complete Domain Model (All Features Combined)
// 1. Sealed hierarchy for domain
sealed interface OrderStatus permits Pending, Processing, Shipped, Delivered, Cancelled {}
record Pending() implements OrderStatus {}
record Processing() implements OrderStatus {}
record Shipped(String trackingNumber) implements OrderStatus {}
record Delivered(LocalDateTime deliveredAt) implements OrderStatus {}
record Cancelled(String reason) implements OrderStatus {}
// 2. Records for data
record OrderItem(String productId, int quantity, BigDecimal price) {}
record Order(
String id,
String customerId,
List<OrderItem> items,
OrderStatus status,
LocalDateTime createdAt
) {
public Order {
items = List.copyOf(items); // Defensive copy
}
public BigDecimal total() {
return items.stream()
.map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// 3. Switch expressions with pattern matching
public static String getStatusMessage(Order order) {
return switch (order.status()) {
case Pending() -> "Your order is pending processing";
case Processing() -> "We're preparing your order";
case Shipped(var tracking) -> "Shipped! Track: " + tracking;
case Delivered(var date) -> "Delivered on " + date.format(DateTimeFormatter.ISO_LOCAL_DATE);
case Cancelled(var reason) -> "Order cancelled: " + reason;
};
}
// 4. var + text blocks for output
public static void printOrderSummary(Order order) {
var items = order.items();
var total = order.total();
var status = getStatusMessage(order);
var summary = """
==== ORDER SUMMARY ====
Order ID: %s
Customer: %s
Status: %s
Items (%d):
%s
Total: $%.2f
Created: %s
""".formatted(
order.id(),
order.customerId(),
status,
items.size(),
items.stream()
.map(item -> " • %s (qty: %d) @ $%.2f each".formatted(
item.productId(), item.quantity(), item.price()))
.collect(Collectors.joining("\n")),
total,
order.createdAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
);
System.out.println(summary);
}
// 5. Usage showing all features together
var order = new Order(
"ORD-2025-001",
"CUST-123",
List.of(
new OrderItem("PROD-A", 2, new BigDecimal("29.99")),
new OrderItem("PROD-B", 1, new BigDecimal("49.99"))
),
new Shipped("UPS-123456"),
LocalDateTime.now().minusDays(2)
);
printOrderSummary(order);
When to Use Each Feature
Use var When:
- Type is obvious from right side:
var user = new User("Alice", 30) - Long generic types:
var map = new HashMap<String, List<Setting>>() - Stream chains:
var filtered = list.stream().filter(...).map(...)
Use Records When:
- Creating data transfer objects (DTOs)
- Modeling immutable value objects
- Building configuration objects
- Returning multiple values from methods
Use Sealed Classes When:
- You have a fixed set of subtypes
- Building domain models with known alternatives
- Want exhaustive pattern matching without defaults
- Controlling API boundaries
Use Pattern Matching When:
- Type checking with immediate variable use
- Simplifying
equals()implementations - Reducing null-check boilerplate
- Guard conditions:
obj instanceof String s && s.length() > 5
Use Switch Expressions When:
- Mapping enum values to results
- Returning values from branches (not just statements)
- Exhaustive enum/sealed type matching
- Complex conditional logic (cleaner than if-else chains)
Use Text Blocks When:
- Multi-line JSON, SQL, HTML, or XML
- Embedded code snippets in strings
- Long error messages
- Test fixtures and templates
Decision Matrix
| Need | Feature | Why |
|---|---|---|
| Reduce type declarations | var | Type inference eliminates noise |
| Replace boilerplate classes | Records | Automatic equals(), hashCode(), toString() |
| Enforce type hierarchy | Sealed | Compiler knows all subtypes |
| Type-safe instanceof | Pattern Matching | One-line type check + cast + bind |
| Conditional return values | Switch Expressions | Cleaner than if-else chains |
| Multi-line strings | Text Blocks | No escape sequence nightmares |
Key Insights
These features work together: Records + sealed classes enable exhaustive pattern matching. var + records = clean data structures. Text blocks + var = template building.
Backward compatible: Existing Java 8 code still works. Adopt features gradually.
Compiler enforcement: Sealed classes and pattern matching catch entire categories of runtime errors at compile-time.
Less ceremony: Together, these features reduce boilerplate by 50-70% compared to Java 8.
Modern idioms: Understanding these features is essential for reading contemporary Java code.
Full Articles & Examples
For detailed coverage, advanced patterns, and comprehensive examples:
👉 Full Part 6 article: blog.9mac.dev/java-17-features-every-senior-developer-should-know-part-6-syntax-cheat-sheet
Complete Series
- Part 1: var Keyword - Type Inference
- Part 2: Records - Immutable Data Carriers
- Part 3: Sealed Classes - Hierarchy Control
- Part 4: Pattern Matching & Switch Expressions
- Part 5: Text Blocks - Multi-line Strings
- Part 6: Syntax Cheat Sheet & Reference Guide
Run the Examples
All code examples are available in the repository with full test coverage:
git clone https://github.com/dawid-swist/blog-9mac-dev-code.git
cd blog-post-examples/java/2025-10-25-java17-features-every-senior-developer-should-know
../../gradlew test
Run tests for specific features:
./gradlew test --tests "*VarExample*"
./gradlew test --tests "*RecordExample*"
./gradlew test --tests "*SealedExample*"
./gradlew test --tests "*PatternExample*"
./gradlew test --tests "*SwitchExample*"
./gradlew test --tests "*TextBlockExample*"
Series Statistics
- Total code examples: 40+ real-world patterns
- Test coverage: 100% with BDD-style test names
- Reading time: 1-2 hours for complete series
- Java versions covered: 10, 14, 15, 16, 17
- Features enabled: 6 major language enhancements
- Lines of boilerplate eliminated: 50-70%
This concludes the "Java 17 Features Every Senior Developer Should Know" series.
All 6 parts are now published with comprehensive examples, best practices, and real-world patterns you can use immediately in production code.
Top comments (1)
This guide really sealed the deal on modern Java—now my code can record wins and switch to cleaner patterns without throwing a fit!