How To Migrate From Java 8 to Java 17 Core Java java Java 17 Java 8 by devs5003 - December 11, 2024December 18, 20240 Last Updated on December 18th, 2024From Java 8 to Java 17, there have been several improvements in the Java coding. However, Java 17 has been released on September 21, 2021, but there are several projects in the industry which are yet to be upgraded. Undoubtedly, it’s not that much easy task for the big applications where in there are billions of lines of code. In this article, we will discuss about ‘How to migrate from Java 8 to Java 17’ & higher versions, especially the upgradation in source code. We will also explore the new features till Java 17 in comparison to Java 8 or earlier versions. Each feature will be demonstrated with an example in Java 8, followed by its equivalent implementation in Java 17. Some features of switch-case introduced in Java 21 are also included in this article which can be helpful in migrating to Java 21 as well. For migration, focus should remain on best practices rather than language updates. Table of Contents Toggle How To Migrate From Java 8 to Java 17?1. Sealed Classes and Interfaces2. Record Classes3. List.of() Factory Method4. Switch Enhancements(A) Object Parameters in Switch (B) Multi-label cases Guard Patterns in case Labels(C) Guarded Pattern in case labels & ‘when’ clause(D) Array Pattern in case labels & ‘when’ clause(E) Object Pattern in case labels & ‘when’ clause(F) Record Pattern in case labels & ‘when’ clause5. Stream Enhancements: takeWhile() and dropWhile()6. Text Blocks 7. Local Variable Type Inference (var)‘var’ with sealed classes, records, and an enhanced switch expression in Java 17 How To Migrate From Java 8 to Java 17? 1. Sealed Classes and Interfaces Java 8 Example In Java 8, defining a class hierarchy required an open inheritance model with abstract classes or interfaces, which can be extended by any class. abstract class Account { abstract double getBalance(); } class SavingsAccount extends Account { private double balance; SavingsAccount(double balance) { this.balance = balance; } @Override double getBalance() { return balance; } } class CheckingAccount extends Account { private double balance; CheckingAccount(double balance) { this.balance = balance; } @Override double getBalance() { return balance; } } Java 17 Implementation If we are using Java 17, we can convert the above code by using Sealed classes/interfaces that restrict inheritance to a predefined set of subclasses. Additionally, we can use record in place of classes (details in the next section) as shown below. sealed interface Account permits SavingsAccount, CheckingAccount { double getBalance(); } record SavingsAccount(double balance) implements Account { @Override public double getBalance() { return balance; } } record CheckingAccount(double balance) implements Account { @Override public double getBalance() { return balance; } } Explanation: Purpose: Ensures a controlled hierarchy, preventing unauthorized inheritance. Improvement: Reduces boilerplate code, enhances code safety and design clarity by restricting subclassing. 2. Record Classes Java 8 Example Data classes in Java 8 required explicit implementation of boilerplate methods. class Transaction { private final int customerId; private final String type; private final double amount; Transaction(int customerId, String type, double amount) { this.customerId = customerId; this.type = type; this.amount = amount; } public int getCustomerId() { return customerId; } public String getType() { return type; } public double getAmount() { return amount; } @Override public String toString() { return "Transaction{customerId=" + customerId + ", type='" + type + "', amount=" + amount + "}"; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Transaction)) return false; Transaction that = (Transaction) o; return customerId == that.customerId && Double.compare(that.amount, amount) == 0 && type.equals(that.type); } @Override public int hashCode() { return Objects.hash(customerId, type, amount); } } Java 17 Implementation In Java 17, we can reduce the above code in a single line as shown below. record Transaction(int customerId, String type, double amount) {} Explanation: Purpose: Removes boilerplate code for getters, setters, toString(), equals(), and hashCode(). Improvement: Provides a clean and concise way to define immutable data classes. 3. List.of() Factory Method Java 8 Example Immutable lists required using Collections.unmodifiableList() as shown below. List<String> customers = Collections.unmodifiableList(Arrays.asList("Java", "Spring Boot", "Angular")); Java 17 Implementation Use of the factory method List.of() simplifies immutable list creation. This feature was introduced in Java 9. As we are talking about migration in Java 17, we can use every new feature that comes into picture from Java 9 to Java 17. List<String> customers = List.of("Java", "Spring Boot", "Angular"); Explanation: Purpose: Provides a concise way to create immutable lists. Improvement: Reduces verbosity and ensures immutability by default. 4. Switch Enhancements (A) Object Parameters in Switch Java 8 Example Switch statements in Java 8 (and earlier versions) only supported primitive types (int, char, byte, short) and specific reference types (String, enum). Object parameters cannot be used directly in a switch statement. In order to achieve similar functionality we were using a combination of if-else and instanceof as shown in the example below. // Using a class hierarchy without sealed classes class Employee {} class Manager extends Employee {} class Developer extends Employee {} class HR extends Employee {} class Admin extends Employee {} public class Java8_ObjectWithoutSwitch { private static String getCategory(Employee employee){ String category; if (employee instanceof Manager) { category = "Managerial Role"; } else if (employee instanceof Developer) { category = "Technical Staff"; } else if (employee instanceof HR) { category = "Human Resources"; } else if (employee instanceof Admin) { category = "Administrative Role"; } else { category = "Unknown Category"; } return category; } public static void main(String[] args) { Employee employee = new Developer(); String category = getCategory(employee); System.out.println(category); } } Java 17 Implementation Here’s an example in Java 17, demonstrating the ability to use objects directly as a switch parameter with -> syntax, and pattern matching. Simply put, we can pass objects in switch condition and this object can be included for different types in switch case labels. // Using sealed classes for structured hierarchy sealed class Employee permits Manager, Developer, HR, Admin {} final class Manager extends Employee {} final class Developer extends Employee {} final class HR extends Employee {} final class Admin extends Employee {} public class Java17SwitchWithObject { private static String getCategory(Employee employee){ String category = switch (employee) { case Manager m -> "Managerial Role"; case Developer d -> "Technical Staff"; case HR h -> "Human Resources"; case Admin a -> "Administrative Role"; default -> "Unknown Category"; }; return category; } public static void main(String[] args) { Employee employee = new Developer(); String category = getCategory(employee); System.out.println(category); } } Java 17 Implementation: Additional Example This example demonstrates the use of various type of objects such as Number, Wrapper classes, Record type and null handling under switch-case statement. In the below example, we are passing an object to the switch condition. This was not possible until Java 17. This object can be of any data type and assigned to a variable as well. We could never pass a null value to switch statements prior to Java 17. If we did so, a NullPointerException was being thrown. Now, if the object we pass is null, we will never get NullPointerException. record Car(String brand, int speed) {} public class Java17SwitchExample { public static void identifyObject(Object obj) { switch (obj) { case null -> System.out.println("It's null"); case Integer i -> System.out.println("It's an Integer: " + i); case String s -> System.out.println("It's a String: " + s); case Number n -> System.out.println("It's a Number other than integer: " + n); case Car(String brand, int speed) -> System.out.println("Car Brand: " + brand + ", Speed: " + speed); default -> System.out.println("Unknown type"); } } public static void main(String[] args) { identifyObject(null); identifyObject(10); identifyObject("Hello"); identifyObject(3.14); identifyObject(new Car("Toyota", 140)); identifyObject(true); } } Output: It's null It's an Integer: 10 It's a String: Hello It's a Number other than integer: 3.14 Car Brand: Toyota, Speed: 140 Unknown type (B) Multi-label cases Guard Patterns in case Labels Java 8 Example Switch statements were limited to simple case labels. Java 8 does not support multi-label cases. int month = 3; String quarter; switch (month) { case 1: case 2: case 3: quarter = "Q1"; break; case 4: case 5: case 6: quarter = "Q2"; break; default: quarter = "Unknown"; } Java 17 Implementation Java 17 supports multi-label cases and objects in switch. int month = 3; String quarter = switch (month) { case 1, 2, 3 -> "Q1"; case 4, 5, 6 -> "Q2"; default -> "Unknown"; }; Java 17 Implementation multi-label cases with Objects Here is another example of multi-label cases with objects in switch. public class Java17MultiLabelCase { public static void main(String[] args) { Employee employee = new Manager(); String category = switch (employee) { case Manager, Developer -> "Technical Staff"; case HR, Admin -> "Non-Technical Staff"; default -> "Unknown Category"; }; System.out.println(category); } } sealed class Employee permits Manager, Developer, HR, Admin {} final class Manager extends Employee {} final class Developer extends Employee {} final class HR extends Employee {} final class Admin extends Employee {} (C) Guarded Pattern in case labels & ‘when’ clause A Guarded Pattern in Java 21 allows us to specify conditions (guards) for switch cases using the ‘when’ keyword. This ensures that a case matches not only by type but also by custom logical conditions. It combines type checking and value-based conditions such as (i > 10). It makes code concise by integrating logic within switch-case label. We can add a Boolean expression right after a pattern label with a when clause. This is called a guarded pattern label. The Boolean expression in the when clause is called a guard. A value matches a guarded pattern label if it matches the pattern and, if so, the guard also evaluates to true. Guarded patterns tightly couple type matching and condition evaluation within the case label, which enhances clarity and reduces boilerplate code. For a separate comprehensive article on Guarded Patterns with examples, kindly visit Guarded Pattern in Java 21. Java 8 Example Without Guarded Pattern switch statements in Java 8 did not support pattern matching or additional conditions. Therefore, we need to use if-else to achieve the functionality as shown in the below code snippet: Object obj = new Transaction(103, "Debit", 150.0); if (obj instanceof Transaction) { Transaction transaction = (Transaction) obj; if ("Debit".equals(transaction.getType()) && transaction.getAmount() < 200) { System.out.println("Small debit transaction"); else if ("Credit".equals(transaction.getType())) { System.out.println("Credit transaction"); } else { System.out.println("Other transaction"); } } Java 17 Implementation with Guarded Pattern Guarded patterns simplify matching with case labels and conditions, reduces the need for nested if-else conditions. It makes the code more concise and readable as shown in the below code snippet. Object obj = new Transaction(103, "Debit", 150.0); String result = switch (obj) { case Transaction(int id, String type, double amount) when "Debit".equals(type) && amount < 200 -> "Small debit transaction"; case Transaction(int id, String type, double amount) when "Credit".equals(type) -> "Credit transaction"; default -> "Other transaction"; }; System.out.println(result); (D) Array Pattern in case labels & ‘when’ clause Below is an example of detecting specific configurations in a multi-monitor setup using Array pattern with ‘when’ keyword. In Java 8, there was no way to directly match or destructure arrays in switch. Explicit loops and checks were necessary. import java.util.Arrays; public class ArrayPatternWithWhenExample { public static void analyzeSetup(Object config) { switch (config) { case String[] devices when devices.length == 1 -> System.out.println("Single Monitor Setup: " + devices[0]); case String[] devices when devices.length > 1 -> System.out.println("Multi-Monitor Setup: " + Arrays.toString(devices)); case int[] resolution when resolution.length == 2 && resolution[0] == 1920 && resolution[1] == 1080 -> System.out.println("Full HD Resolution"); case int[] resolution when resolution.length == 2 -> System.out.println("Custom Resolution: " + Arrays.toString(resolution)); default -> System.out.println("Unknown Configuration"); } } public static void main(String[] args) { analyzeSetup(new String[]{"Monitor1"}); analyzeSetup(new String[]{"Monitor1", "Monitor2"}); analyzeSetup(new int[]{1920, 1080}); analyzeSetup(new int[]{2560, 1440}); } } Output: Single Monitor Setup: Monitor1 Multi-Monitor Setup: [Monitor1, Monitor2] Full HD Resolution Custom Resolution: [2560, 1440] In the above example, the when keyword is used to refine array matches based on length or specific content. This approach is ideal for analyzing structured data like arrays in practical scenarios. (E) Object Pattern in case labels & ‘when’ clause Let’s take an example of classifying numerical input in an analytics application based on specific ranges or characteristics. public class ObjectPatternWithWhenExample { public static void classifyNumber(Number num) { switch (num) { case Integer i when i >= 0 && i <= 100 -> System.out.println("Small Integer: " + i); case Integer i when i > 100 -> System.out.println("Large Integer: " + i); case Double d when d >= 0.0 && d <= 1.0 -> System.out.println("Normalized Double: " + d); case Double d -> System.out.println("General Double: " + d); default -> System.out.println("Uncategorized Number."); } } public static void main(String[] args) { classifyNumber(50); classifyNumber(150); classifyNumber(0.75); classifyNumber(42.42); } } Output: Small Integer: 50 Large Integer: 150 Normalized Double: 0.75 General Double: 42.42 (F) Record Pattern in case labels & ‘when’ clause Let’s take an example of a Record class ‘User’ that processes user data in a record management system.. The switch statement identifies users based on their age. record User(String name, int age) {} public class RecordPatternExample { public static void processUser(Object obj) { switch (obj) { case User(String name, int age) when age >= 18 -> System.out.println(name + " is an adult."); case User(String name, int age) -> System.out.println(name + " is a minor."); default -> System.out.println("Unknown object."); } } public static void main(String[] args) { processUser(new User("Alice", 25)); processUser(new User("Bob", 15)); processUser("RandomString"); } } Output: Alice is an adult. Bob is a minor. Unknown object. 5. Stream Enhancements: takeWhile() and dropWhile() Java 8 Example Filtering data required explicit conditions with filter(). List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> lessThanFour = numbers.stream() .filter(n -> n < 4) .collect(Collectors.toList()); Java 17 Implementation takeWhile() and dropWhile() simplify data processing. List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> lessThanFour = numbers.stream() .takeWhile(n -> n < 4) .toList(); List<Integer> greaterThanThree = numbers.stream() .dropWhile(n -> n <= 3) .toList(); Explanation: Purpose: Enables efficient and declarative processing of sorted streams. Improvement: Reduces complexity by handling sequential conditions directly. 6. Text Blocks Here’s an example demonstrating Text Blocks introduced in Java 13 (available as a preview) and finalized in Java 15, compared to how the same functionality was handled in Java 8. Java 8 Example Multiline strings required explicit formatting. public class TextBlockExampleJava8 { public static void main(String[] args) { String sqlQuery = "SELECT id, name, email \n" + "FROM users \n" + "WHERE active = 1 \n" + "ORDER BY name ASC;"; System.out.println("SQL Query:" +sqlQuery); } } The above representation has following limitations: Requires explicit concatenation (+) for multiline strings. Use of \n for line breaks. Poor readability and maintainability, especially for long text or formatted content. Java 17 Implementation Text blocks simplify multiline string handling. public class TextBlockExampleJava17 { public static void main(String[] args) { String sqlQuery = """ SELECT id, name, email FROM users WHERE active = 1 ORDER BY name ASC; """; System.out.println("SQL Query:" +sqlQuery); } } The above representation has following improvements: Text blocks use “”” delimiters for defining multi-line strings. No need for explicit concatenation or \n for line breaks. Maintains natural formatting, improving readability. Reduces the chance of syntax errors in complex queries or JSON/XML structures. 7. Local Variable Type Inference (var) The ‘var’ keyword, introduced in Java 10, allows local variable type inference, making code more concise without explicitly stating the type. It automatically infers the type from the assigned value, making code less verbose. It helps in reducing boilerplate code, especially for long type names such as Map<String, List<Employee>>. It should be used when the type is obvious or improves clarity. Here is an example of using var: import java.util.HashMap; import java.util.Map; public class VarExample { public static void main(String[] args) { // Using 'var' for type inference var employeeSalaries = new HashMap<String, Double>(); employeeSalaries.put("Alice", 75000.0); employeeSalaries.put("Bob", 65000.0); for (var entry : employeeSalaries.entrySet()) { System.out.println("Employee: " + entry.getKey() + ", Salary: " + entry.getValue()); } } } ‘var’ with sealed classes, records, and an enhanced switch expression in Java 17 Here’s an example demonstrating the use of var with sealed classes, records, and an enhanced switch expression in Java 17: // Define sealed hierarchy sealed interface Employee permits Manager, Developer, Intern {} record Manager(String name, int teamSize) implements Employee {} record Developer(String name, String programmingLanguage) implements Employee {} record Intern(String name, String mentor) implements Employee {} public class Java17VarExample { public static void main(String[] args) { Employee employee = new Manager("Alice", 10); // Using var in a switch expression var category = switch (employee) { case Manager m -> "Manager of team size: " + m.teamSize(); case Developer d -> "Developer skilled in: " + d.programmingLanguage(); case Intern i -> "Intern mentored by: " + i.mentor(); }; System.out.println(category); // Using var with records var dev = new Developer("Bob", "Java"); System.out.println("Developer Details: " + dev); } } However, we must ensure it doesn’t reduce code readability or introduce ambiguity, particularly for collaborative projects. Key considerations remain: Use var when the type is obvious or inferred from the right-hand side. Avoid var if it compromises readability, especially with complex or ambiguous types. Local variable declarations can improve readability by removing redundant details but may restrict clarity if important information is ignored. Therefore, we should use this feature with proper judgement, as there is no strict rule for when it should or shouldn’t be applied. Here are the Local Variable Type Inference: Style Guidelines that explain the pros and cons of explicit versus implicit type declarations and offers practical guidelines for the effective use of var declarations. ♥ You may also go through important interview questions & answers explained on various other topics by visiting our Java Quiz section. References: https://docs.oracle.com/en/java/javase/17/language/ https://docs.oracle.com/en/java/javase/21/language/ Related
In this article, we will discuss about ‘How to migrate from Java 8 to Java 17’ & higher versions, especially the upgradation in source code. We will also explore the new features till Java 17 in comparison to Java 8 or earlier versions. Each feature will be demonstrated with an example in Java 8, followed by its equivalent implementation in Java 17. Some features of switch-case introduced in Java 21 are also included in this article which can be helpful in migrating to Java 21 as well. For migration, focus should remain on best practices rather than language updates. Table of Contents Toggle How To Migrate From Java 8 to Java 17?1. Sealed Classes and Interfaces2. Record Classes3. List.of() Factory Method4. Switch Enhancements(A) Object Parameters in Switch (B) Multi-label cases Guard Patterns in case Labels(C) Guarded Pattern in case labels & ‘when’ clause(D) Array Pattern in case labels & ‘when’ clause(E) Object Pattern in case labels & ‘when’ clause(F) Record Pattern in case labels & ‘when’ clause5. Stream Enhancements: takeWhile() and dropWhile()6. Text Blocks 7. Local Variable Type Inference (var)‘var’ with sealed classes, records, and an enhanced switch expression in Java 17 How To Migrate From Java 8 to Java 17? 1. Sealed Classes and Interfaces Java 8 Example In Java 8, defining a class hierarchy required an open inheritance model with abstract classes or interfaces, which can be extended by any class. abstract class Account { abstract double getBalance(); } class SavingsAccount extends Account { private double balance; SavingsAccount(double balance) { this.balance = balance; } @Override double getBalance() { return balance; } } class CheckingAccount extends Account { private double balance; CheckingAccount(double balance) { this.balance = balance; } @Override double getBalance() { return balance; } } Java 17 Implementation If we are using Java 17, we can convert the above code by using Sealed classes/interfaces that restrict inheritance to a predefined set of subclasses. Additionally, we can use record in place of classes (details in the next section) as shown below. sealed interface Account permits SavingsAccount, CheckingAccount { double getBalance(); } record SavingsAccount(double balance) implements Account { @Override public double getBalance() { return balance; } } record CheckingAccount(double balance) implements Account { @Override public double getBalance() { return balance; } } Explanation: Purpose: Ensures a controlled hierarchy, preventing unauthorized inheritance. Improvement: Reduces boilerplate code, enhances code safety and design clarity by restricting subclassing. 2. Record Classes Java 8 Example Data classes in Java 8 required explicit implementation of boilerplate methods. class Transaction { private final int customerId; private final String type; private final double amount; Transaction(int customerId, String type, double amount) { this.customerId = customerId; this.type = type; this.amount = amount; } public int getCustomerId() { return customerId; } public String getType() { return type; } public double getAmount() { return amount; } @Override public String toString() { return "Transaction{customerId=" + customerId + ", type='" + type + "', amount=" + amount + "}"; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Transaction)) return false; Transaction that = (Transaction) o; return customerId == that.customerId && Double.compare(that.amount, amount) == 0 && type.equals(that.type); } @Override public int hashCode() { return Objects.hash(customerId, type, amount); } } Java 17 Implementation In Java 17, we can reduce the above code in a single line as shown below. record Transaction(int customerId, String type, double amount) {} Explanation: Purpose: Removes boilerplate code for getters, setters, toString(), equals(), and hashCode(). Improvement: Provides a clean and concise way to define immutable data classes. 3. List.of() Factory Method Java 8 Example Immutable lists required using Collections.unmodifiableList() as shown below. List<String> customers = Collections.unmodifiableList(Arrays.asList("Java", "Spring Boot", "Angular")); Java 17 Implementation Use of the factory method List.of() simplifies immutable list creation. This feature was introduced in Java 9. As we are talking about migration in Java 17, we can use every new feature that comes into picture from Java 9 to Java 17. List<String> customers = List.of("Java", "Spring Boot", "Angular"); Explanation: Purpose: Provides a concise way to create immutable lists. Improvement: Reduces verbosity and ensures immutability by default. 4. Switch Enhancements (A) Object Parameters in Switch Java 8 Example Switch statements in Java 8 (and earlier versions) only supported primitive types (int, char, byte, short) and specific reference types (String, enum). Object parameters cannot be used directly in a switch statement. In order to achieve similar functionality we were using a combination of if-else and instanceof as shown in the example below. // Using a class hierarchy without sealed classes class Employee {} class Manager extends Employee {} class Developer extends Employee {} class HR extends Employee {} class Admin extends Employee {} public class Java8_ObjectWithoutSwitch { private static String getCategory(Employee employee){ String category; if (employee instanceof Manager) { category = "Managerial Role"; } else if (employee instanceof Developer) { category = "Technical Staff"; } else if (employee instanceof HR) { category = "Human Resources"; } else if (employee instanceof Admin) { category = "Administrative Role"; } else { category = "Unknown Category"; } return category; } public static void main(String[] args) { Employee employee = new Developer(); String category = getCategory(employee); System.out.println(category); } } Java 17 Implementation Here’s an example in Java 17, demonstrating the ability to use objects directly as a switch parameter with -> syntax, and pattern matching. Simply put, we can pass objects in switch condition and this object can be included for different types in switch case labels. // Using sealed classes for structured hierarchy sealed class Employee permits Manager, Developer, HR, Admin {} final class Manager extends Employee {} final class Developer extends Employee {} final class HR extends Employee {} final class Admin extends Employee {} public class Java17SwitchWithObject { private static String getCategory(Employee employee){ String category = switch (employee) { case Manager m -> "Managerial Role"; case Developer d -> "Technical Staff"; case HR h -> "Human Resources"; case Admin a -> "Administrative Role"; default -> "Unknown Category"; }; return category; } public static void main(String[] args) { Employee employee = new Developer(); String category = getCategory(employee); System.out.println(category); } } Java 17 Implementation: Additional Example This example demonstrates the use of various type of objects such as Number, Wrapper classes, Record type and null handling under switch-case statement. In the below example, we are passing an object to the switch condition. This was not possible until Java 17. This object can be of any data type and assigned to a variable as well. We could never pass a null value to switch statements prior to Java 17. If we did so, a NullPointerException was being thrown. Now, if the object we pass is null, we will never get NullPointerException. record Car(String brand, int speed) {} public class Java17SwitchExample { public static void identifyObject(Object obj) { switch (obj) { case null -> System.out.println("It's null"); case Integer i -> System.out.println("It's an Integer: " + i); case String s -> System.out.println("It's a String: " + s); case Number n -> System.out.println("It's a Number other than integer: " + n); case Car(String brand, int speed) -> System.out.println("Car Brand: " + brand + ", Speed: " + speed); default -> System.out.println("Unknown type"); } } public static void main(String[] args) { identifyObject(null); identifyObject(10); identifyObject("Hello"); identifyObject(3.14); identifyObject(new Car("Toyota", 140)); identifyObject(true); } } Output: It's null It's an Integer: 10 It's a String: Hello It's a Number other than integer: 3.14 Car Brand: Toyota, Speed: 140 Unknown type (B) Multi-label cases Guard Patterns in case Labels Java 8 Example Switch statements were limited to simple case labels. Java 8 does not support multi-label cases. int month = 3; String quarter; switch (month) { case 1: case 2: case 3: quarter = "Q1"; break; case 4: case 5: case 6: quarter = "Q2"; break; default: quarter = "Unknown"; } Java 17 Implementation Java 17 supports multi-label cases and objects in switch. int month = 3; String quarter = switch (month) { case 1, 2, 3 -> "Q1"; case 4, 5, 6 -> "Q2"; default -> "Unknown"; }; Java 17 Implementation multi-label cases with Objects Here is another example of multi-label cases with objects in switch. public class Java17MultiLabelCase { public static void main(String[] args) { Employee employee = new Manager(); String category = switch (employee) { case Manager, Developer -> "Technical Staff"; case HR, Admin -> "Non-Technical Staff"; default -> "Unknown Category"; }; System.out.println(category); } } sealed class Employee permits Manager, Developer, HR, Admin {} final class Manager extends Employee {} final class Developer extends Employee {} final class HR extends Employee {} final class Admin extends Employee {} (C) Guarded Pattern in case labels & ‘when’ clause A Guarded Pattern in Java 21 allows us to specify conditions (guards) for switch cases using the ‘when’ keyword. This ensures that a case matches not only by type but also by custom logical conditions. It combines type checking and value-based conditions such as (i > 10). It makes code concise by integrating logic within switch-case label. We can add a Boolean expression right after a pattern label with a when clause. This is called a guarded pattern label. The Boolean expression in the when clause is called a guard. A value matches a guarded pattern label if it matches the pattern and, if so, the guard also evaluates to true. Guarded patterns tightly couple type matching and condition evaluation within the case label, which enhances clarity and reduces boilerplate code. For a separate comprehensive article on Guarded Patterns with examples, kindly visit Guarded Pattern in Java 21. Java 8 Example Without Guarded Pattern switch statements in Java 8 did not support pattern matching or additional conditions. Therefore, we need to use if-else to achieve the functionality as shown in the below code snippet: Object obj = new Transaction(103, "Debit", 150.0); if (obj instanceof Transaction) { Transaction transaction = (Transaction) obj; if ("Debit".equals(transaction.getType()) && transaction.getAmount() < 200) { System.out.println("Small debit transaction"); else if ("Credit".equals(transaction.getType())) { System.out.println("Credit transaction"); } else { System.out.println("Other transaction"); } } Java 17 Implementation with Guarded Pattern Guarded patterns simplify matching with case labels and conditions, reduces the need for nested if-else conditions. It makes the code more concise and readable as shown in the below code snippet. Object obj = new Transaction(103, "Debit", 150.0); String result = switch (obj) { case Transaction(int id, String type, double amount) when "Debit".equals(type) && amount < 200 -> "Small debit transaction"; case Transaction(int id, String type, double amount) when "Credit".equals(type) -> "Credit transaction"; default -> "Other transaction"; }; System.out.println(result); (D) Array Pattern in case labels & ‘when’ clause Below is an example of detecting specific configurations in a multi-monitor setup using Array pattern with ‘when’ keyword. In Java 8, there was no way to directly match or destructure arrays in switch. Explicit loops and checks were necessary. import java.util.Arrays; public class ArrayPatternWithWhenExample { public static void analyzeSetup(Object config) { switch (config) { case String[] devices when devices.length == 1 -> System.out.println("Single Monitor Setup: " + devices[0]); case String[] devices when devices.length > 1 -> System.out.println("Multi-Monitor Setup: " + Arrays.toString(devices)); case int[] resolution when resolution.length == 2 && resolution[0] == 1920 && resolution[1] == 1080 -> System.out.println("Full HD Resolution"); case int[] resolution when resolution.length == 2 -> System.out.println("Custom Resolution: " + Arrays.toString(resolution)); default -> System.out.println("Unknown Configuration"); } } public static void main(String[] args) { analyzeSetup(new String[]{"Monitor1"}); analyzeSetup(new String[]{"Monitor1", "Monitor2"}); analyzeSetup(new int[]{1920, 1080}); analyzeSetup(new int[]{2560, 1440}); } } Output: Single Monitor Setup: Monitor1 Multi-Monitor Setup: [Monitor1, Monitor2] Full HD Resolution Custom Resolution: [2560, 1440] In the above example, the when keyword is used to refine array matches based on length or specific content. This approach is ideal for analyzing structured data like arrays in practical scenarios. (E) Object Pattern in case labels & ‘when’ clause Let’s take an example of classifying numerical input in an analytics application based on specific ranges or characteristics. public class ObjectPatternWithWhenExample { public static void classifyNumber(Number num) { switch (num) { case Integer i when i >= 0 && i <= 100 -> System.out.println("Small Integer: " + i); case Integer i when i > 100 -> System.out.println("Large Integer: " + i); case Double d when d >= 0.0 && d <= 1.0 -> System.out.println("Normalized Double: " + d); case Double d -> System.out.println("General Double: " + d); default -> System.out.println("Uncategorized Number."); } } public static void main(String[] args) { classifyNumber(50); classifyNumber(150); classifyNumber(0.75); classifyNumber(42.42); } } Output: Small Integer: 50 Large Integer: 150 Normalized Double: 0.75 General Double: 42.42 (F) Record Pattern in case labels & ‘when’ clause Let’s take an example of a Record class ‘User’ that processes user data in a record management system.. The switch statement identifies users based on their age. record User(String name, int age) {} public class RecordPatternExample { public static void processUser(Object obj) { switch (obj) { case User(String name, int age) when age >= 18 -> System.out.println(name + " is an adult."); case User(String name, int age) -> System.out.println(name + " is a minor."); default -> System.out.println("Unknown object."); } } public static void main(String[] args) { processUser(new User("Alice", 25)); processUser(new User("Bob", 15)); processUser("RandomString"); } } Output: Alice is an adult. Bob is a minor. Unknown object. 5. Stream Enhancements: takeWhile() and dropWhile() Java 8 Example Filtering data required explicit conditions with filter(). List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> lessThanFour = numbers.stream() .filter(n -> n < 4) .collect(Collectors.toList()); Java 17 Implementation takeWhile() and dropWhile() simplify data processing. List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); List<Integer> lessThanFour = numbers.stream() .takeWhile(n -> n < 4) .toList(); List<Integer> greaterThanThree = numbers.stream() .dropWhile(n -> n <= 3) .toList(); Explanation: Purpose: Enables efficient and declarative processing of sorted streams. Improvement: Reduces complexity by handling sequential conditions directly. 6. Text Blocks Here’s an example demonstrating Text Blocks introduced in Java 13 (available as a preview) and finalized in Java 15, compared to how the same functionality was handled in Java 8. Java 8 Example Multiline strings required explicit formatting. public class TextBlockExampleJava8 { public static void main(String[] args) { String sqlQuery = "SELECT id, name, email \n" + "FROM users \n" + "WHERE active = 1 \n" + "ORDER BY name ASC;"; System.out.println("SQL Query:" +sqlQuery); } } The above representation has following limitations: Requires explicit concatenation (+) for multiline strings. Use of \n for line breaks. Poor readability and maintainability, especially for long text or formatted content. Java 17 Implementation Text blocks simplify multiline string handling. public class TextBlockExampleJava17 { public static void main(String[] args) { String sqlQuery = """ SELECT id, name, email FROM users WHERE active = 1 ORDER BY name ASC; """; System.out.println("SQL Query:" +sqlQuery); } } The above representation has following improvements: Text blocks use “”” delimiters for defining multi-line strings. No need for explicit concatenation or \n for line breaks. Maintains natural formatting, improving readability. Reduces the chance of syntax errors in complex queries or JSON/XML structures. 7. Local Variable Type Inference (var) The ‘var’ keyword, introduced in Java 10, allows local variable type inference, making code more concise without explicitly stating the type. It automatically infers the type from the assigned value, making code less verbose. It helps in reducing boilerplate code, especially for long type names such as Map<String, List<Employee>>. It should be used when the type is obvious or improves clarity. Here is an example of using var: import java.util.HashMap; import java.util.Map; public class VarExample { public static void main(String[] args) { // Using 'var' for type inference var employeeSalaries = new HashMap<String, Double>(); employeeSalaries.put("Alice", 75000.0); employeeSalaries.put("Bob", 65000.0); for (var entry : employeeSalaries.entrySet()) { System.out.println("Employee: " + entry.getKey() + ", Salary: " + entry.getValue()); } } } ‘var’ with sealed classes, records, and an enhanced switch expression in Java 17 Here’s an example demonstrating the use of var with sealed classes, records, and an enhanced switch expression in Java 17: // Define sealed hierarchy sealed interface Employee permits Manager, Developer, Intern {} record Manager(String name, int teamSize) implements Employee {} record Developer(String name, String programmingLanguage) implements Employee {} record Intern(String name, String mentor) implements Employee {} public class Java17VarExample { public static void main(String[] args) { Employee employee = new Manager("Alice", 10); // Using var in a switch expression var category = switch (employee) { case Manager m -> "Manager of team size: " + m.teamSize(); case Developer d -> "Developer skilled in: " + d.programmingLanguage(); case Intern i -> "Intern mentored by: " + i.mentor(); }; System.out.println(category); // Using var with records var dev = new Developer("Bob", "Java"); System.out.println("Developer Details: " + dev); } } However, we must ensure it doesn’t reduce code readability or introduce ambiguity, particularly for collaborative projects. Key considerations remain: Use var when the type is obvious or inferred from the right-hand side. Avoid var if it compromises readability, especially with complex or ambiguous types. Local variable declarations can improve readability by removing redundant details but may restrict clarity if important information is ignored. Therefore, we should use this feature with proper judgement, as there is no strict rule for when it should or shouldn’t be applied. Here are the Local Variable Type Inference: Style Guidelines that explain the pros and cons of explicit versus implicit type declarations and offers practical guidelines for the effective use of var declarations. ♥ You may also go through important interview questions & answers explained on various other topics by visiting our Java Quiz section.
References: https://docs.oracle.com/en/java/javase/17/language/ https://docs.oracle.com/en/java/javase/21/language/
References: https://docs.oracle.com/en/java/javase/17/language/ https://docs.oracle.com/en/java/javase/21/language/
References: https://docs.oracle.com/en/java/javase/17/language/ https://docs.oracle.com/en/java/javase/21/language/
References: https://docs.oracle.com/en/java/javase/17/language/ https://docs.oracle.com/en/java/javase/21/language/