IntelliJ IDEA Java

Evolution of the Switch Construct in Java—Why Should you Care?

Even though defining conditions in code is one of the programming basics, developers didn’t use the old switch statements often because this feature was quite constrained in its capabilities. This is not the case anymore. With the addition of switch expressions and Pattern Matching, the switch construct has evolved a lot—it can return values, switch over multiple types, compare objects and not just constants in case labels, and much more. It can replace long if-else constructs and older switch statements with concise code, for use cases that you might not know about. It has fewer bugs, is easier to read, write, and understand, and is a charm to work with. 

Don’t worry if all these new features overwhelm you. IntelliJ IDEA can help you get started with using the modern switch construct. It can detect if-else code/ old switch style that could be replaced with the new switch, even if you don’t know anything about its syntax or usage. IntelliJ IDEA can also prompt you to use the switch in the correct or improved way.

In this blog post, I’ll cover all these changes using hands-on coding examples to highlight their benefits. I’ll also address a few of the frequently asked questions by developers, such as, why does it seem that the switch has been changing over a long time, why, and how. 

1. Understanding the Bigger Picture

Before we deep dive into the sample code, let us answer a few frequently asked questions by developers on this topic.

1.1 What changed with the switch construct, how and when?

The humble switch construct has come a long way, and is now available in two flavors – Switch Expressions and Switch Statements. The first change was initiated via Switch Expressions, (Java 14), and the addition of Pattern Matching for Switch (Java 21) has revolutionized the ways in which it can be used. In Java 23, you might be able to use primitive types in patterns too. 

Unlike a switch statement, Switch expressions can return a value, without needing a break statement at the end of each case label. Pattern matching for switch significantly reduces the count of lines of code you need for an equivalent if-else construct. It enables you to switch on a lot of data types rather than just a handful of primitives, enums and String (as was the case with the older switch constructs). With pattern matching for switch, you can handle null values as a case label, match different data types or record patterns and initialize pattern variables. You can also refine the conditions in case labels by using when, and much more. Targeted for Java 23, switch patterns would be able to include primitive data types in patterns.

However, changes to the switch construct over multiple Java version releases have left some developers confused – regarding the different changes that were added to it or dropped, current state of affairs and the ways it can be used now. 

1.2 Why are developers confused about the changes made to switch?

With Java’s six-month release cadence, changes to its existing language features, like the switch construct, or addition of new features like the Pattern Matching, are introduced as  preview feature – features that are complete, but impermanent. 

Such features might take up to two or more Java versions to become permanent in the language. For example, Switch Expressions was introduced in Java 12, but was permanently added in Java in its version 14. Sometimes it is confusing to remember when a feature was introduced or if it is still in preview. Though developers are encouraged to try out or experiment with the preview features, using them in production code is not recommended.

Also, preview features might change over two or more Java versions, until they are permanently added to the Java language. For example, when the Switch Expressions were introduced as a preview language feature in Java 12, its case label used the keyword break to return a value, which was later changed to the keyword yield. Though these changes are few, they might add up to the confusion – what was changed, and when? Talking about ‘when’, guarded patterns in Java 17 and 18 used & to define a guarded pattern, which has been changed to the usage of when since Java version 19. A preview feature could be dropped altogether if sufficient feedback suggests that. Raw String Literals is one such example. 

1.3 Why were ‘switch’ constructs not used enough in the past?

Sequence, condition and iteration—are the three basic programming structures to create instructions or algorithms in our code bases.

When it comes to defining conditions; nothing beats the if/ if-else constructs. You have used them to define the simplest or the most complex conditions, using any type of variables or constants. So, we developers usually preferred if/ if-else constructs over other options like the (old) switch construct and ternary operators for defining conditions because the later two had very limited capabilities.

1.4 Why bother using ‘switch’ now?

With all the changes that I mentioned in the previous sections, switch is the new cool kid on the block. It can also be used in use cases where only if-else could be used earlier. Also, developers realized that if/ if-else constructs have disadvantages too—the long if/else constructs are not easy to read. They can’t force developers to define a set of conditions at the language level (you might need an else condition even when it is not needed). Also, optimizing code that uses if/ if-else is difficult and has O(n) time complexity, as opposed to O(1) time complexity for switch. 

Let’s work with coding examples to understand benefits of using the new evolved switch in everyday coding.

2. Returning values with Switch Expressions

As compared to the ‘old’ switch constructs, switch expressions can return a value, starting with Java 14. Why is this a big deal? Let’s find out using a few commonly occurring use cases.

2.1. Replace switch statement with concise switch expressions

Imagine a common use case of calculating a discount for an order via a formula (lambda) that depends on a customer’s loyalty card type and their loyalty points. Before switch expressions, you might code this functionality using a switch statement (that can’t return a value) as shown in the code below.

It was common to switch over the card type owned by a customer, create a local variable, say, result, at the beginning of the method, conditionally assign the lambda to calculate the discount in each switch case, and return its value at the end of the method (assume that the variable a represents the order total and variable b represents the points accumulated in the customer’s loyalty card):

enum CardType {SILVER, GOLD, PLATINUM, DIAMOND}

private static BiFunction<Double, Integer, Double> getOrderDiscountFormula(CardType cardType) {
    BiFunction<Double, Integer, Double> result;
    switch (cardType) {
        case SILVER:
            result = (a, b) -> (a * 0) + b;
            break;
        case GOLD:
            result = (a, b) -> (a * .05) + b;
            break;
        case PLATINUM:
            result = (a, b) -> (a * 0.1) + b * 2;
            break;
        case DIAMOND:
            result = (a, b) -> (a * 0.15) + b * 3;
            break;
        default:
            throw new IllegalArgumentException("Unexpected value: " + cardType);
    }
    return result;
}

Whenever you see, hear or experience the word repetition (in the coding or development ecosystem), beware! That is the perfect place for bugs to hide.

The preceding code has multiple issues:

  • Code Repetition: Each case label needs a break statement to exit ‘switch’ after code in a case label executes. Code result = is repeated in each case label.
  • Potential logical errors: If you miss adding the break statement in any of the case labels, it will result in a default fall-through. What that means is, the flow control falls to the next case label (until it reaches another break statement) and executes the corresponding code, assigning another value to the variable result, which would be incorrect.
  • Writing unreachable code: In this specific example, the default case label would never execute because the enum CardType defines four values. Still you must write it or the code won’t compile (error message-variable ‘result’ might not have been initialized).

Let’s see how we could address all of these issues by replacing the switch statement with switch expressions, via the following gif :

Here’s the final code from the preceding gif (as text) for your reference:

private static BiFunction<Double, Integer, Double> getOrderDiscountFormula(CardType cardType) {
    return switch (cardType) {
        case SILVER -> (a, b) -> (a * 0) + b;
        case GOLD -> (a, b) -> (a * .05) + b;
        case PLATINUM -> (a, b) -> (a * 0.1) + b * 2;
        case DIAMOND -> (a, b) -> (a * 0.15) + b * 3;
    };
}

By replacing a switch construct with switch expressions, we could:

  • Make this code concise and easier to understand.
  • Use a switch expression to return a value from a method.
  • Drop the duplicate code to assign value to result in each use case (result = ).
  • Delete the default case labels, since all possible values for the enum CardType were already addressed.

That’s not all. With switch switch expressions, you can define multiple case labels for a switch branch, to execute the same code for multiple switch case labels.

For an example, imagine a new CardType, say, CORPORATE, is added to the enum. Even though this card might offer different benefits, it is possible that it offers the same benefits on the order discounts as offered by an existing card type, say, the PLATINUM card. In this case, you would be able to add the label CORPORATE to the existing switch branch for PLATINUM, as follows (multiple values in a switch label were not allowed before the introduction of switch expressions):

enum CardType { SILVER, GOLD, PLATINUM, DIAMOND, CORPORATE}

private static BiFunction<Double, Integer, Double> getOrderDiscountFormula(CardType cardType) {
    return switch (cardType) {
        case SILVER -> (a, b) -> (a * 0) + b;
        case GOLD -> (a, b) -> (a * .05) + b;
        case PLATINUM, CORPORATE -> (a, b) -> (a * 0.1) + b * 2;
        case DIAMOND -> (a, b) -> (a * 0.15) + b * 3;
    };
}

It is quite likely that you might see the code example we started with in this section coded using an if-else construct instead of an switch statement. Are there any benefits to converting an if-else statement to a switch expression. Let’s find out in the next section.

2.2. If feasible, prefer Switch expressions over if/else statements for improved readability

If/else statements let you define complex conditions, which may not be feasible with switch constructs. But, if an if/else construct could be replaced with a switch construct, go for it because it would make your code easier to understand.

Let’s rewrite the code sample from the previous section using an if-else statement instead of a switch statement, as follows:

   private static BiFunction<Double, Integer, Double> getOrderDiscountFormula(CardType cardType) {
        BiFunction<Double, Integer, Double> result;
        if (cardType == CardType.SILVER) {
            result = (a, b) -> (a * 0) + b;
        } else if (cardType == CardType.GOLD) {
            result =  (a, b) -> (a * .05) + b;
        } else if (cardType == CardType.PLATINUM) {
            result = (a, b) -> (a * 0.1) + b * 2;
        } else if (cardType == CardType.DIAMOND) {
            result = (a, b) -> (a * 0.15) + b * 3;
        } else {
            throw new IllegalArgumentException("Unexpected CardType value: " + cardType);
        }
        return result; 
    }

You might ask-what are the issues with the preceding code?

Since we humans are used to reading text from top to bottom, an if-else statement reads like a long winding answer to a question. If you call the preceding method with a value CardType.DIAMOND, it could seem like answering multiple questions, such as, is the value CardType.SILVER, if not, is it GOLD, PLATINUM or DIAMOND?

This flow control is shown using the gif below that uses a debugger. Even though the inline variable value shows the value true next to the condition ‘cardType == CardType.DIAMOND’:, the control flow moves from top to bottom:

The next obvious question is – which is better in terms of performance. In theory, an if-else construct is difficult to optimize since it has O(n) time complexity; switch has O(1) time complexity. In practice, compiler optimizations may close this gap.

You could manually convert the preceding code to use a switch expression. But, it is error-prone. I’d recommend using IntelliJ IDEA’s context action-Replace ‘if’ with ‘switch’, as follows:

Quick note-if you have set the language level to 17 or later in IntelliJ IDEA, you’d also notice the label null with the default label after the preceding conversion.

When you use the debugger to see the flow of the control flow for a switch construct, you’ll notice it requires much less keystrokes, as shown in the following gif:

2.3. Refactor code and use switch expressions

Often using just a switch expression might not be enough to see how it could improve the readability of your code. Consider (modified) code of a method from an open source library that was probably written prior to the existence of switch expressions:

    
	public boolean getValueAsBoolean(boolean defaultValue) throws IOException {
        MyClass myclass = this.currentToken;
        if (myclass != null) {
            switch (myclass.id()) {
                case 6:
                    String str = this.getText().trim();
                    if ("true".equals(str)) {
                        return true;
                    }

                    if ("false".equals(str)) {
                        return false;
                    }

                    if ("null".equals(str)) {
                        return false;
                    }
                    break;
                case 7:
                    return this.getIntValue() != 0;
                case 8:
                default:
                    break;
                case 9:
                    return true;
                case 10:
                case 11:
                    return false;
                case 12:
                    Object value = this.getEmbeddedObject();
                    if (value instanceof Boolean) {
                        return (Boolean)value;
                    }
            }
        }
        return defaultValue;
    }

The preceding code could be improved by – extracting code for the label 6 to a separate method, refactoring the initial conditions and replacing the switch statement with a switch, as follows:

    public boolean getValueAsBoolean2(boolean defaultValue) throws IOException {
        if (currentToken == null || currentToken.id() == 8) {
            return defaultValue;
        }
        return switch (currentToken.id()) {
            case 6 -> checkAndGetBooleanValue();
            case 7 -> getIntValue() != 0;
            case 9 -> true;
            case 10, 11 -> false;
            case 12 -> getEmbeddedObject() instanceof Boolean value && (boolean) value;
            default -> defaultValue;
        };
    }

    private boolean checkAndGetBooleanValue() throws IOException {
        String trimmedText = getText().trim();
        return switch (trimmedText) {
            case "true", "null" -> true;
            case "false" -> false;
            default -> throw new IllegalArgumentException("Unexpected value: " + trimmedText);
        };
    }

2.4. Using Stream API with switch expressions as method arguments

Since switch expressions can return a value, you could pass it as arguments to methods too. It also means being able to use different coding approaches, such as being able to use the Stream API, instead of a for loop. For an example, the following code:

private static StringBuilder handleSpecialCharacters(StringBuilder sb, String segment) {
    int i = 0;
    for (int end = segment.length(); i < end; ++i) {
        char c = segment.charAt(i);
        switch (c) {
            case '+':
                sb.append("~2");
                break;
            case '-':
                sb.append("~9");
                break;
            case '*':
                sb.append("~5");
                break;
            default:
                sb.append(c);
                break;
        }
    }
    return sb;
}

Could be coded using switch expressions and Stream API, as follows:

private static StringBuilder handleSpecialCharacters(StringBuilder sb, String segment) {
    segment.chars()	
           .mapToObj(c -> (char) c)
           .map(c -> switch(c){
               case '+' -> "~2";
               case '-' -> "~9";
               case '*' -> "~5";
               default -> String.valueOf(c);
           })
           .forEach(sb::append);
    return sb;
}

Using a single line lambda is usually preferred over multiline lambdas. For the preceding code, you could extract the switch expression to a method to achieve it, as follows:

private static StringBuilder handleSpecialCharacters2(StringBuilder sb, String segment) {
    segment.chars()
           .mapToObj(c -> (char) c)
           .map(c -> mapCharacter(c))
           .forEach(sb::append);
    return sb;
}

private static String mapCharacter(Character c) {
    return switch (c) {
        case '/' -> "~1";
        case '~' -> "~0";
        case '*' -> "~5";
        default -> String.valueOf(c);
    };
}

In all the preceding example code segments, you noticed the usage of an arrow syntax for switch expressions for its case labels. Does it imply it could never use the colon syntax? Let’s find out in the next section.

2.5. Switch statements vs. Switch expressions and Colon Syntax vs. Arrow Syntax

The switch statements co-exist with the switch expressions and they both can use the colon and arrow syntax with their case labels. Switch expressions could use the colon syntax and still return a value using the yield keyword. The following image shows examples of switch statements and switch expressions using both of these syntax to help you to compare them:

Introduction of switch expressions to Java in version 14 didn’t expand the count of data types that a switch construct could switch on. This still remained a major hurdle that prevented it from being used widely by developers. Don’t worry! The good news is that with pattern matching (production feature in Java 21), you can switch on many more data types. Let’s check out this benefit with many more in the next section.

3. Pattern Matching for switch – comparing objects and more

Pattern matching for switch is a production feature in Java 21, which means you can use it in your code without any fears of it being changed in the future Java versions.

Let’s list how Pattern Matching for switch enhances the switch statements and expressions, helping you to write complex data queries with ease:

  • What can you switch on: You can switch on many more types than just the primitive data types, wrapper classes, enums and Strings.
  • Type Test Pattern: You can test the type of a variable in a switch label
  • Pattern variable: Testing of the variable type is followed by declaring a pattern variable.
  • Refining Type Test pattern: You can refine Type test patterns in switch labels by adding a condition using the keyword when.
  • No explicit casting required: You can call methods on the pattern variable without any explicit casting.
  • What can switch labels include: Switch labels are not limited to comparing constant values. You can check null, constants, and type of an instance in a switch case.
  • Record Patterns in switch labels: Switch labels allow you to use record patterns too.

If you are new to Pattern Matching in switch or Pattern matching in general, I’d recommend you to refer to this section in my blog post on Java 17 and IntelliJ IDEA, which covers all the basics like what is pattern matching, how to use it with the instanceof operator and switch constructs, what are their benefits, and many more topics like how it reduces the cognitive load for you.

In this blog post, I’ll cover the benefits of using pattern matching in switch using hands-on examples.

3.1 Switch on user defined data types

Here’s a common use case. Imagine you need to create a method that determines item wise discount for items in an invoice, using the following rules:

40% discount – all books

20% discount – all electronics

30% discount – all apparel

10% discount – rest of the items

Assume you create the following interface and records to model these types:

sealed interface SaleItem{}
record Book(String title, double price) implements SaleItem { }
record Electronics(String name, double price) implements SaleItem { }
record Apparel(String type, String size, double price) implements SaleItem { }

The gif below shows how you can code a method, say, computeDiscount, which accepts a type of SaleItem, switches on it and, depending on its actual type, calculates the discount using the business rules defined above.

The following gif also highlights IntelliJ IDEA’s support for pattern matching with switch (with sealed types). You can use the .switch postfix operator on the variable item, which passes the variable item as a selector pattern to switch. Switch is used as an expression here (visible via a return keyword preceding its usage), and the interface SaleItem is defined as a sealed type. IntelliJ IDEA can determine these conditions and generate all the case labels for this switch expression by invoking Context Actions (Alt + Enter) and choosing ‘Create missing branches’. This support in the IDE is quite helpful if you are new to this Java feature or its syntax:

Here’s the resultant code for your reference:

public static double computeDiscount(SaleItem item) {
    return switch (item) {
        case Apparel apparel -> 0.3 * apparel.price();
        case Book book -> 0.4 * book.price();
        case Electronics electronics -> 0.2 * electronics.price();
    };
}

The preceding code has multiple enhancements, which are specific to pattern matching for switch:

  • The code is concise.
  • The type of selector expression to switch is a user-defined type, that is, SaleItem. This was not possible earlier.
  • Each switch branch uses a Type pattern (Apparel/ Book/ Electronics) and declares a pattern variable (book, electronics and apparel). When the type of selector expression matches with the type defined in a switch branch, that pattern variable is initialized (you don’t need explicit casting).

Since a switch construct can switch over many more types, it makes sense to check if the value of the selector pattern is null within the switch construct. With pattern matching for switch, you can define null as a case label, as follows:

public static double computeDiscount(SaleItem item) {
    return switch (item) {
        case Apparel apparel -> 0.3 * apparel.price();
        case Book book -> 0.4 * book.price();
        case Electronics electronics -> 0.2 * electronics.price();
        case null -> 0;
    };
}

Prior to addition of Pattern Matching, you might have coded the preceding method as follows:

    public static double computeDiscount(SaleItem item) {
        if (item instanceof Apparel) {
            Apparel apparel = (Apparel)item;
            return 0.3 * apparel.price();
        } else if (item instanceof Book) {
            Book book = (Book)item;
            return 0.4 * book.price();
        } else if (item instanceof Electronics) {
            Electronics electronics = (Electronics)item;
            return 0.2 * electronics.price();
        }
        else {
            return 0;
        }
    }

One of the main issues with the preceding code is repetition. All if-else blocks use the instanceof operator to check the type of the method parameter item, and then create another variable, explicitly casting it to the type it was checked against the instanceof operator in the preceding line of code. This is repetition overkill. The good news is that this if-else statement could be replaced with the modern switch construct.

If you are using IntelliJ IDEA, you would notice that it can detect such code and notify you by highlighting the if keyword. On invoking context actions (Alt + Enter), it prompts you to-Replace ‘if’ with ‘switch’, as shown in the following gif:

One of other benefits of using pattern matching for switch is that it saves you from writing code for situations that would never occur. If you use if/else construct for the above code, you would still need to code the else part, irrespective of the fact there are no subtypes of SaleItem, apart from Apparel, Book and Electronics. The default clause is not required for code version that uses pattern matching with switch.

Imagine the business rules to calculate item wise discount change for apparels based on their sizes. Lets see how you can add conditions with a Type pattern in the next section.

3.2 Simplifying the code to calculate discounted price (using WHEN in switch labels)

The new business rules to calculate discount on apparels are:

60% discount – all apparel of size XXS

30% discount – all apparel of sizes other than XXS.

One of the obvious ways to implement this business logic is to add a condition for on the right side of -> for Type pattern Apparel, using if-else to check if the size equals XXS or not, and yield to return a value::

    public static double computeDiscount(SaleItem item) {
        return switch (item) {
            case Apparel apparel -> {
                if (apparel.size().equals("XXS")) {
                    yield 0.6 * apparel.price();
                } else {
                    yield 0.3 * apparel.price();
                }
            }
            case Book book -> 0.4 * book.price();
            case Electronics electronics -> 0.2 * electronics.price();
            case null, default -> 0;
        };
    }

Let’s see if we could simplify the preceding code by using a ternary operator instead of an if-else construct as follows:

    public static double computeDiscount(SaleItem item) {
        return switch (item) {
            case Apparel apparel -> apparel.size().equals("XXS") ? 
                                        0.6 * apparel.price() : 
                                        0.3 * apparel.price();
            case Book book -> 0.4 * book.price();
            case Electronics electronics -> 0.2 * electronics.price();
            case null, default -> 0;
        };
    }

The last code looks better. But wait! With Pattern Matching for switch you can add a condition by using when in a case label. If you have worked with databases and queries, you’ll love and prefer the following code that uses when (it is also better in terms of readability):

public static double computeDiscount(SaleItem item) {
    return switch (item) {
        case Apparel apparel when apparel.size().equals("XXS")  -> 0.6 * apparel.price();
        case Apparel apparel                                    -> 0.3 * apparel.price();
        case Book book                                          -> 0.4 * book.price();
        case Electronics electronics                            -> 0.2 * electronics.price();
        case null, default                                      -> 0;
    };
}

The keyword when is followed by an expression that returns a boolean value. This implies that you could add more complex conditions, or even call a method that returns a boolean value.

Take a moment to register that the condition that includes when with the type Apparel in the preceding code is more specific than the switch branch that follows it. It is compilation error to swap the order of these case labels (as shown in the following gif):

In the preceding code, the record Apparel defined multiple components, which are referred to in the switch branch. You also use Record pattern to deconstruct it to its components in pattern variables- to make it easy to refer to them.

3.3 Record patterns – reduce repetitions in code

In the preceding code, the switch label that calculates the discount for Apparel for size XXS needs to use the pattern variable apparel repeatedly to access its components size and price. You could use record patterns to deconstruct a record to its components, rewrite the code from the above section as follows:

    public static double computeDiscount(SaleItem item) {
        return switch (item) {
            case Apparel (String type, String size, double price) 
                    when size.equals("XXS")                         -> 0.6 * price;
            case Apparel apparel                                    -> 0.3 * apparel.price();
            case Book book                                          -> 0.4 * book.price();
            case Electronics electronics                            -> 0.2 * electronics.price();
            case null, default                                      -> 0;
        };
    }

Record patterns could be used in multiple use case, for example to define recursive method calls, as follows:

sealed interface TwoDimensional {}
record Point (int x, int y) implements TwoDimensional { }
record Line    ( Point start, 
                 Point end) implements TwoDimensional { }
record Triangle( Point pointA, 
                 Point pointB, 
                 Point PointC) implements TwoDimensional { }
record Square  ( Point pointA, 
                 Point pointB, 
                 Point PointC, 
                 Point pointD) implements TwoDimensional { }

static int process(TwoDimensional twoDim) {
   return switch (twoDim) {
       case Point(int x, int y) -> x + y;
       case Line(Point a, Point b) -> process(a) + process(b);
       case Triangle(Point a, Point b, Point c) -> 
                                 process(a) + process(b) + process(c);
       case Square(Point a, Point b, Point c, Point d) -> 
                                 process(a) + process(b) + process(c) + process(d);
   };
}

If you are new to Record Patterns, please check out my detailed blog post on this topic to find out more about how to use it with nested records, var type, generics and more.

The preceding code (calculating item wise discounts) uses record patterns in the case labels, specifying all the components of a record. However, all of these component variables are not used. Let’s check if it is mandatory to specify all the component variables in the next section.

3.4 Ignoring unused variables by using Unnamed Pattern in Record Patterns

Added as preview feature in Java 21,Unnamed variables and Patterns continues to be a preview feature in Java 22, It allows you to replace unused variables in record patterns and other places via an underscore, that is, _.

Let’s replace the unused component of the Record Apparel, that is, String type, with an _, in the record pattern, as follows:

    public static double computeDiscount(SaleItem item) {
        return switch (item) {
            case Apparel (_, String size, double price) 
                    when size.equals("XXS")			-> 0.6 * price;
            case Apparel apparel                            -> 0.3 * apparel.price();
            case Book book                                  -> 0.4 * book.price();
            case Electronics electronics                    -> 0.2 * electronics.price();
            case null, default                              -> 0;
        };
    }

This feature comes in quite handy for records and nested that might use record patterns with multiple unused variables, such as in the following example:

record Name       (String fName, String lName) { }
record PhoneNumber(String areaCode, String number) { }
record Country    (String countryCode, String countryName) { }
record Passenger  (Name name,
                   PhoneNumber phoneNumber,
                   Country from,
                   Country destination) { }

public class GeoMaps {
    boolean checkFirstNameAndCountryCodeAgain (Object obj) {
        if (obj instanceof Passenger(Name (String fName, _),
                                     _,
                                     _,
                                     Country (String countryCode, _) )) {

            if (fName != null && countryCode != null) {
                return fName.startsWith("Simo") && countryCode.equals("PRG");
            }
        }
        return false;
    }
}

4. Looking forward – adding primitives in patterns and switch

Languages as we know, whether natural languages or programming languages, change and evolve. Moving forward, you would soon be able to use primitive type patterns in switch constructs. Targeted to be released as a preview feature in Java 23, Check out this link for further details.

Summary

The humble switch construct has come a long way with the introduction of switch expressions and pattern matching to it. They add clarity and conciseness to your code, which increases manifold when coupled with other new Java features such as the sealed classes, and coding practices like refactoring. Switch Expressions became and production feature in Java 14 and Pattern Matching and record patterns are a production feature since Java 21. You can use all these features in your codebase without any worries of them changing later.

Watch out for further improvements to the switch construct via the unnamed patterns and addition of primitives to patterns.

If you haven’t tried using the new evolved switch in your codebase, give it a try, you won’t regret it :-)

image description