instanceof
expressions, making conditions easier to read and understand.instanceof
: Seamlessly integrates with existing instanceof
checks, allowing for conditional actions based on record content in a type-safe manner.Before Java 21:
Before, it was technically possible to use a switch
to suss out different types, but it was a very bad approach, and it involved manual casting (which can lead to class cast exceptions).
Object obj = // some object switch (obj.getClass().getSimpleName()) { case "String": String s = (String) obj; System.out.println("String of length: " + s.length()); break; case "Integer": Integer i = (Integer) obj; System.out.println("Integer value: " + i); break; default: System.out.println("Unknown type"); }
After Java 21:
With the new pattern matching, the switch
automatically casts the object to that type and immediately makes its components available for local use.
Object obj = // some object switch (obj) { case String s -> System.out.println("String of length: " + s.length()); case Integer i -> System.out.println("Integer value: " + i); default -> System.out.println("Unknown type"); }
Notice how we don’t have to call the properties via the object, we have immediate access to those values.
BankAccount
-class and a CheckingAccount
-subclass and SavingsAccount
-subclass (no implementation required).BankApp
-class and start writing in the main
-method.SavingsAccount
-object or new CheckingAccount
-object to a BankAccount
variable.response
, then print that response.public class BankApp { public static void main(String[] args) { Scanner keyboard = new Scanner(System.in); System.out.print("Enter the type of account you want to create: "); var account = switch (keyboard.nextLine()) { case "checking" -> new CheckingAccount(); case "savings" -> new SavingsAccount(); default -> new BankAccount(); }; String response = switch (account) { case CheckingAccount c -> "You have a checking account"; case SavingsAccount s -> "You have a savings account"; default -> "You have a bank account"; }; System.out.println(response); } }
public class BankAccount { }
public class CheckingAccount extends BankAccount { }
public class SavingsAccount extends BankAccount { }
switch
case are a powerful combination!switch (employee) { case FullTimeEmployee(String name, double salary) -> System.out.println(name + " is a full-time employee with a salary of $" + salary); case PartTimeEmployee(String name, double hourlyRate) -> System.out.println(name + " is a part-time employee earning $" + hourlyRate + " per hour"); case Intern(String name, String school) -> System.out.println(name + " is an intern from " + school); default -> System.out.println("Unknown employee type"); }
double area = switch (shape) { case Circle(double radius) -> Math.PI * radius * radius; case Rectangle(double width, double height) -> width * height; case Triangle(double base, double height) -> 0.5 * base * height; default -> 0; };
You can even do fancy stuff like extra conditional checks at field level:
switch (user) { case User(String name, "admin") -> System.out.println(name + " has all permissions."); case User(String name, "editor") -> System.out.println(name + " can create, edit, and delete their posts."); case User(String name, "viewer") -> System.out.println(name + " can only view content."); default -> System.out.println("Role is undefined."); }
This way of adding conditions to the pattern matching is known as a guarded pattern, and they let us get away with avoiding extra if
-conditions in our switch cases.
Shape[] shapes = new Shape[] { new Circle(5), new Rectangle(3, 4), new Rectangle(4, 3), new Circle(3), }; for (Shape shape : shapes) { if(shape instanceof Circle(double radius)) { System.out.printf("Circle with radius %.1f", radius); } else if(shape instanceof Rectangle(double width, double height)) { System.out.printf("Rectangle with width %.1f and height %.1f", width, height); } System.out.printf(" and area %.1f and perimeter %.1f\n", shape.calcArea(), shape.calcArea()); }
Expense
-interface and implementing records as shown in the UML (no extra logic required).ExpensesApp
-class, write a calcExpense
-method that accepts an Expense
-object and, using a switch expression with record pattern matching, returns a double (the actual monetary value of that expense, which for each sub-record is different).main
-method, make a collection of expenses, and, in one line (using streams), calculate the total expenses, then print it.Shape[] shapes = new Shape[]{ new Circle(5), new Rectangle(3, 4), new Rectangle(4, 3), new Circle(3), }; for (Shape shape : shapes) { String info = switch (shape) { case Circle(double radius) -> String.format("Circle with radius %.1f", radius); case Rectangle(double width, double height) -> String.format("Rectangle with width %.1f and height %.1f", width, height); default -> "Unknown shape"; }; System.out.printf("%s and area %.1f and perimeter %.1f\n", info, shape.calcArea(), shape.calcArea()); }
public class ExpenseApp { public static void main(String[] args) { double totalExpenses = Set.of( new Mortgage("My house", 1000, 0.05, 5), new OnlineSubscription("Netflix", "https://www.netflix.com", 10), new Product("iPhone", 1000), new Product("MacBook", 1000) ).stream().mapToDouble(ExpenseApp::calcExpense).sum(); System.out.printf("Total expenses: $%,.2f\n", totalExpenses); } private static double calcExpense(Expense expense){ return switch (expense) { case Mortgage(String propertyName, double monthlyPayment, double interestRate, int numberOfYears) -> monthlyPayment * numberOfYears * (1 + interestRate); case OnlineSubscription(String name, String url, double monthlyPrice) -> monthlyPrice; case Product(String name, double price) -> price; default -> 0; }; } }
public interface Expense { }
public record Mortgage(String propertyName, double monthlyPayment, double interestRate, int numberOfYears) implements Expense { }
public record OnlineSubscription(String name, String url, double monthlyPrice) implements Expense { }
public record Product(String name, double price) implements Expense { }
switch
-statements, they become a powerful tool for more expressive and safer code.public sealed class Payment permits CardPayment, BankTransfer { // Common payment methods } public void processPayment(Payment payment) { switch (payment) { case CreditCardPayment ccp -> System.out.println("Processing credit card payment"); case BankTransfer bt -> System.out.println("Processing bank transfer"); default -> System.out.println("Unknown payment type"); } }
Payment
and executes a specific block of code for each type.switch
statement is exhaustive, preventing runtime errors due to unhandled types.switch
, enabling automatic type casting and direct access to components, which simplifies how developers handle different data types.switch
cases, providing a straightforward way to access record components without manual extraction.