DEV Community

Sowmiya Ravikumar
Sowmiya Ravikumar

Posted on

1

Pattern Matching in Switch - Java

Switch has come a long way from being a limited control structure to now supporting expressive features like switch expressions (finalized in Java 14) and pattern matching (finalized in Java 21).

Pain points

See how verbose and complex the code below looks. We are

  • null proofing
  • checking the type using instanceof
  • manually casting

This means that if you need to introduce more types in the future, you will end up with more if-else branches, manual checks and boilerplate code.

static void verboseDisplay(Object obj) {
    String result = "";
    if(Objects.nonNull(obj)) {
        if (obj instanceof String) {
            String str = (String) obj;
            int len = str.length();
            result = String.format("We have got string %s of length %d", str, len);
        } else if (obj instanceof Integer) {
            Integer num = (Integer) obj;
            result = String.format("We have got integer %d", num);
        } else {
            result = String.format("We have got %s of type %s", obj, obj.getClass().getName());
        }
    }
    System.out.println(result);
}
Enter fullscreen mode Exit fullscreen mode

Doesn't this look better?

I have refactored the above code using switch expression and pattern matching.

static void cleanDisplay(Object obj) {
    String result  = switch(obj) {
        case null -> "";
        case String str -> String.format("We have got a string %s of length %d", str, str.length());
        case Integer num -> String.format("We have got an integer %d", num);
        default -> String.format("We have got %s of type %s", obj, obj.getClass().getName());
    };
    System.out.println(result);
}
Enter fullscreen mode Exit fullscreen mode

We have moved away from

  1. if-else branching
  2. explicit null checks
  3. type checks using instanceof
  4. manual type casting
  5. repetitive assignments inside case blocks

Let's break it down

  • We have used switch expression with arrow(->) and default case, instead of traditional switch with colon (:) and break statements. Since Java 21, null can be added as a valid case label.

đź’ˇTip: Its a good practice to add the null case at the beginning before non-null cases for an early exit and avoiding NullPointerException. If you dont include null case, cleanDisplay(null) will cause NPE as before.

  • In the pattern label case String str , obj is the target object to evaluate, String str is the Type Pattern and str is the pattern variable. Here comes the power of pattern matching: the compiler checks if the target matches the type, if it is, it safely casts the target to the type and initializes the pattern variable with the data of the target. No explicit type checks and type casting needed.
  • Using Switch expressions, result can be returned directly from switch block, making code cleaner and functional.

Scope of the pattern variable

If the target matches the type pattern, then the pattern variable gets initialized and its scope lies within the block of the code which introduced it.

Consider this example

static void simpleScope(Object obj) {
    String result =  switch (obj) {
        case Integer i -> {
            System.out.println("I can be access inside this block");
            yield String.format("%d - Integer Type", i);
        }
        case String s1 when s1.length() <= 5 -> String.format("%s - String Type of length <= 5", s1);
        case String s2 -> String.format("%s - String Type of length > 5", s2);
        default -> "Unknown Type";
    };
    System.out.println(result);
}
Enter fullscreen mode Exit fullscreen mode

If the type of obj is an Integer, the scope of the pattern variable i is valid within the block to right side of the arrow.
If the type of obj is String, scope of the pattern variable s1 is valid in the when clause as well as in statement to the right of the arrow.

Consider the below example for instanceof

static void scopeTest(Object obj) {
    if(!(obj instanceof String s)) {
        System.out.println("s cannot be accessed here");
        return;
    }
    System.out.printf("s can very well be accessed here %s", s);
}
Enter fullscreen mode Exit fullscreen mode

If type of obj is String, pattern varaible s can be accessed anywhere inside the method. For other types, s is never initialized because the pattern dont't match.

Guarded pattern label

In this example, did you notice the second case label? Unlike the first case label with a type pattern, this one also has a when clause followed by a boolean expression. These type of case label is referred as Guarded pattern label.

It's a guard

If obj is of type String and the length of the string is lesser than or equal to 5, then the statement to the right of 2nd case label gets executed. Otherwise (if type of obj is String and boolean expression str.length() <= 5 evaluates to false), then the statement to the right of 3rd case label gets executed.

Switch now supports primitive types

⚠️ This is still a preview feature introduced in Java 23 and re-introduced with no change in Java 24. Switch supports String, enum, reference types and also some primitive types like char, byte, short and int, as well as their auto-boxed classes. However, as part of this preview feature long, float, double, and boolean primitive types are also now supported.

Look at this example

static void primitiveTypes(float fVal) {
    switch (fVal) {
        case 0f, 1f -> System.out.println("We have got a float 0 or 1");
        case float f -> System.out.printf("We have got a float %f%n", f);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, switch accepts primitive type - float. 1st case matches constant float values and 2nd case matches any float values other than (0f or 1f).

đź’ˇTip: Enable preview feature while running this code.

Unconditionally exact casting

If you would call primitiveTypes method with below signature, compiler would implicitly cast int to float.

int num = 1;
primitiveTypes(num);
Enter fullscreen mode Exit fullscreen mode

Whereas, consider primitiveTypeInt method in which switch accepts int and you would call the method with float, đź’Ą compiler will throw error.

static void primitiveTypeInt(int fVal) {
    switch (fVal) {
        case 0, 1 -> System.out.println("We have got a float 0 or 1");
        case int f -> System.out.printf("We have got a float %d%n", f);
    }
}

primitiveTypeInt(1f);
Enter fullscreen mode Exit fullscreen mode

Case dominance

In this example, if you swap the cases, case float f will dominate case 0f, 1f and compiler will throw error because case 2 is never reachable.

So, as a rule of thumb, follow this order:

  1. Constant case labels
  2. Guarded pattern labels
  3. Non-guarded pattern labels
static void orderedCases(float fVal) {
    switch (fVal) {
        case 0f, 1f -> System.out.println("We have got a float 0 or 1");
        case float f when f <= 5 -> System.out.printf("We have got a float > 1 and <= 5: %f%n", f);
        case float f -> System.out.println("We have got a float > 5");

    }
}
Enter fullscreen mode Exit fullscreen mode

We have covered some of the important features of switch introduced in recent Java releases. This is just part 1 of the blog, I will try to cover record patterns, top-level and nested patterns, switch exhaustiveness and much more in part 2. Thank you for reading up until this point. Let me know if you find it useful, that will keep me motivated to write more.

If you like to connect, or share your thoughts and views, feel free to drop a message at LinkedIn or Email

References:

Image of Stellar post

How a Hackathon Win Led to My Startup Getting Funded

In this episode, you'll see:

  • The hackathon wins that sparked the journey.
  • The moment JosĂ© and Joseph decided to go all-in.
  • Building a working prototype on Stellar.
  • Using the PassKeys feature of Soroban.
  • Getting funded via the Stellar Community Fund.

Watch the video

Top comments (0)

Jetbrains image

Is Your CI/CD Server a Prime Target for Attack?

57% of organizations have suffered from a security incident related to DevOps toolchain exposures. It makes sense—CI/CD servers have access to source code, a highly valuable asset. Is yours secure? Check out nine practical tips to protect your CI/CD.

Learn more

đź‘‹ Kindness is contagious

Value this insightful article and join the thriving DEV Community. Developers of every skill level are encouraged to contribute and expand our collective knowledge.

A simple “thank you” can uplift someone’s spirits. Leave your appreciation in the comments!

On DEV, exchanging expertise lightens our path and reinforces our bonds. Enjoyed the read? A quick note of thanks to the author means a lot.

Okay