DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • Java String: A Complete Guide With Examples
  • Generics in Java and Their Implementation
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically
  • High-Performance Java Serialization to Different Formats

Trending

  • How to Convert Between PDF and TIFF in Java
  • *You* Can Shape Trend Reports: Join DZone's Software Supply Chain Security Research
  • Why Documentation Matters More Than You Think
  • Segmentation Violation and How Rust Helps Overcome It
  1. DZone
  2. Coding
  3. Languages
  4. Pattern Matching for Switch

Pattern Matching for Switch

Let's see how this feature preview has evolved up to Java 19

By 
Claudio De Sio Cesari user avatar
Claudio De Sio Cesari
·
May. 22, 22 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
4.9K Views

Join the DZone community and get the full member experience.

Join For Free

According to some surveys such as that of JetBrains, version 8 of Java is currently the most used by developers all over the world, despite being a 2014 release.

What you are reading is the one in a series of articles titled “Going beyond Java 8”, inspired by the contents of my books “Java for Aliens” (English) and “Il nuovo Java” (Italian). These articles will guide the reader step by step to explore the most important features introduced starting from version 9. The aim is to make the reader aware of how important it is to move forward from Java 8, explaining the enormous advantages that the latest versions of the language offer.

In this article, we will see an interesting novelty introduced in version 17 as a feature preview and which will probably be made official in version 20. This is the second part of a complex feature known as pattern matching. If the first part has changed forever how we used the instanceof operator for the better (see this dedicated article), the second improves the switch construct, actually already improved in version 14 with the introduction of a new syntax based on the arrow notation, and the possibility of using it as an expression (see this dedicated article).

This post is quite technical and requires knowledge of some features recently added to the language. If necessary, therefore, we recommend that you first read the articles on pattern matching for instanceof, the new switch, feature preview, and sealed types, which are preparatory to full understanding of the following.

Pattern Matching

With the introduction of pattern matching for instanceof in Java 16, we have defined a pattern as composed of:

  • A predicate: a test that looks for the matching of an input with its operand. As we will see, this operand is a type (in fact we call it type pattern).
  • One or more binding variables (also called constraint variables or pattern variables): these are extracted from the operand depending on the test result. With pattern matching, a new scope for variables has been introduced, the binding scope, which guarantees the visibility of the variable only where the predicate is verified.

In practice, a pattern is therefore a synthetic way of presenting a complex solution.

The concept is very similar to that concept behind regular expressions. In this case, however, the pattern is based on the recognition of a certain type using the instanceof operator, and not on a certain sequence of characters to be found in a string.

The New switch

The switch construct was revised in version 12 as a feature preview and made official in version 14. The revision of the construct introduced a less verbose and more robust syntax based on the arrow operator ->, and it is also possible to use switch as an expression. You can learn more about it in the dedicated article. The construct thus became more powerful, useful, and elegant. However, we still had the constraint to pass only certain types to a switch as input:

  • The primitive types byte, short, int and char.
  • The corresponding wrapper types: Byte, Short, Integer and Character.
  • The String type
  • An enumeration type.

In the future, it is planned to make the construct even more useful by adding new types to the above list, such as the primitive types float, double and boolean. Also, in the next versions we will have a switch construct that will allow us to preview the deconstruction of objects feature. Currently, it is still early to talk about it, but in the meantime, Java is advancing quickly step by step and from version 17 it is already possible to preview a new version of the switch construct, which allows us to pass an object of any type as input. To enter a certain case clause, we will use pattern matching for instanceof.

The (Brand) New switch

Let's consider the following method:

Java
 
public static String getInfo(Object object) {
  if (object instanceof Integer integer) {
    return (integer < 0 ? "Negative" : "Positive") + " integer";
  }
  if (object instanceof String string) {
    return "String of length " + string.length();
  }
  return "Unknown";
}


It takes an Object type parameter as input and therefore accepts any type, and using the instanceof operator returns a particular descriptive string. Although the pattern matching for instanceof has allowed us to avoid the usual step that included the declaration of a reference and its cast, the code is still not very readable, is inelegant and error-prone. So, we can rewrite the previous method using the pattern matching applied to a switch expression:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
    case String s -> "String of length " + s.length();
    default -> "Unknown";
  };
}


The code is now more concise, readable, applicable, functional, and elegant, but let's take our time to analyze it.

Note that, unlike the switch construct we have always used, in the previous example the validation of a certain case definition will not be based on the equals operator whose second operand is a constant, but on the instanceof operator whose second operand is a type.

In practice, the code that follows the arrow operator -> of the case Integer i will be executed, if the object parameter is of the Integer type. Within this code, the Integer type binding variable i will point to the same object that the reference object points to.

Instead, will be executed the code that follows the arrow operator -> of the case String s if the object parameter is of type String. Within this code, the binding variable s of type String will point to the same object that the reference object points to.

Finally, we will enter the default clause if the object parameter is neither of type String nor of type Integer.

The construct is completed by the default clause whose code will be executed if the object variable points to an object other than both Integerand String .

In order to master the pattern matching for switch , however, it is also necessary to know a series of properties that will be presented in the next sections.

Remember that in versions 17, 18 and 19, this feature is still in preview. This means that to compile and execute an application that makes use of pattern matching for switch, certain options must be specified as described in the article dedicated to feature preview.

Exhaustiveness

Note that the default clause is required in order not to get a compilation error. In fact, the switch construct with pattern matching includes exhaustiveness (also known as completeness: completeness of coverage of all possible options), among its properties. In this way, the construct is more robust and less prone to errors.

Due to the backward compatibility that has always characterized Java, it was not possible to modify the compiler in such a way that it claims completeness even with the original switch construct. In fact, such a change would prevent the compilation of many pre-existing projects. However, it is foreseen for the next versions of Java that the compiler prints a warning in the case of implementations of the "old" switch that do not cover all possible cases. All in all, the most important IDEs already warn programmers in these situations.

Note that, in theory, we could also substitute the clause:

Java
 
default -> "Unknown";


with the equivalent:

Java
 
case Object o -> "Unknown";


In fact, even in this case we would have covered all the possible options. Consequently, the compiler will not allow you to insert both of these clauses in the same construct. 

Dominance

If we try to move the case Object o clause before the clauses related to the Integer and String types:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Object o -> "Unknown";
    case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
    case String s -> "String of length " + s.length();
  };
}


we will get the following compile-time errors:

Plain Text
 
error: this case label is dominated by a preceding case label
       case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
            ^
error: this case label is dominated by a preceding case label
       case String s -> "String of length " + s.length();
            ^


In fact, another property of the pattern matching for switch known as dominance, causes the compiler to consider the case Integer i and case String s unreachable, because they are "dominated" by the case Object o. In practice, this last clause includes the conditions of the next two which would therefore never be reached.

This behavior is very similar to conditions on the catch clauses of a try statement. A more generic catch clause could dominate subsequent catch clauses, causing a compiler error. Also, in such cases we need to place the dominant clause after the others.

Dominance and default Clause

Unlike ordinary case clauses, the default clause does not necessarily have to be inserted as the last clause. In fact, it is perfectly legal to insert the default clause as the first statement of a switch construct without altering its functioning. The following code compiles without errors:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    default -> "Unknown";
    case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
    case String s -> "String of length " + s.length();
  };
}

This actually also applies to the classic switch construct, and it is also the reason why it is advisable to insert a break statement in the defaultclause as well. In fact, inadvertently adding a new clause after the defaultwithout the break statement, could cause an unwanted fall-through.

Guarded Pattern

We can also specify patterns composed with boolean expressions using the && operator, that are called guarded patterns (and the boolean expression is called guard). For example, we can rewrite the previous example as follows:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Integer i && i < 0 -> "Negative integer"; // guarded pattern
    case Integer i -> "Positive integer";
    case String s -> "String of length " + s.length();
    default -> "Unknown";
  };
}


The code is more readable and intuitive.

In version 19 (third preview) based on developer feedback, the && operator has been replaced by the when clause (new contextual keyword). So, the previous code from version 19 onwards needs to be rewritten like this:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Integer i when i < 0 -> "Negative integer"; // guarded pattern
    case Integer i -> "Positive integer";
    case String s -> "String of length " + s.length();
    default -> "Unknown";
  };
}


Note that if we invert the clauses concerning the integers as follows:

 
   case Integer i -> "Positive integer"; //questo pattern "domina" il successivo
   case Integer i when i < 0 -> "Negative integer"; 


we will get a dominance error:

Plain Text
 
error: this case label is dominated by a preceding case label
            case Integer i when i < 0 -> "Negative integer";
                 ^

Fall-Through

We have already seen in the article dedicated to the new switch, how the new syntax based on the arrow operator -> allows us to use a unique case clause to manage multiple case clauses in the same way. In practice, we simulate the use of an OR operator || avoiding using a fall-through. For example, we can write:

Java
 
Month month = getMonth();
String season = switch(month) {
    case DECEMBER, JANUARY, FEBRUARY -> "winter";
    case MARCH, APRIL, MAY -> "spring";
    case JUNE, JULY, AUGUST -> "summer";
    case SEPTEMBER, OCTOBER, NOVEMBER -> "autumn";
};


The syntax is much more concise, elegant, and robust.

When we use pattern matching, however, the situation changes. It is not possible to use multiple patterns in the clauses of the switch construct to handle different types in the same way. For example, the following method:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Integer i, Float f -> "This is a number";
    case String s -> "String of length " + s.length();
    default -> "Unknown";
  };
}


would produce a compilation error:

Plain Text
 
error: illegal fall-through to a pattern
            case Integer i, Float f -> "This is a number";
                            ^


In fact, due to the definition of binding variables we talked about in the article dedicated to pattern matching for instanceof, the code after the arrow operator could use both the variable i and the variable f, but one of them will certainly not be initialized. So, it was therefore chosen not to make this code compile in order to have a more robust construct.

Note that the error message points out that this code is invalid because it defines an illegal fall-through. This is because the previous code is equivalent to the following which, not using the syntax based on the arrow operator ->, makes use of the fall-through:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case Integer i: // manca il break: fall-through
    case Float f: 
      yield "This is a number";
    break;
    case String s: 
      yield "String of length " + s.length();
    break;
    default: 
      yield "Unknown";
    break;
  };
}


Obviously, also this code can be compiled successfully.

Null Check

Since the possibility of passing any type to a switchconstruct has been introduced, we should first check that the input reference is not null. But rather than preceding the switchconstruct with the usual null check:

Java
 
  if (object == null) {
    return "Null!";
  }


we can instead use a new elegant clause to handle the case where the object parameter is null:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    case null -> "Null!"; // controllo di nullità
    case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
    case String s -> "String of length " + s.length();
    default -> "Unknown";
  };
}


The case null clause allows us to avoid the usual tedious nullcheck we are used to. This clause is optional, but since it is now always possible to pass a null reference to a switch, if we do not insert one explicitly, the compiler will insert one for us implicitly, whose code will throw a NullPointerException.

Dominance and case null

Note that, as the default clause does not have to be the last of a switchclause, the case null clause does not need to be at the top of the construct. Consequently, even for this clause the rule of dominance is not applicable. It is perfectly legal to move the case null as the last line of the switch, as it is legal to have the default clause as the first clause without affecting the functionality of the construct:

Java
 
public static String getInfo(Object object) {
  return switch (object) {
    default -> "Unknown";
    case Integer i -> (i < 0 ? "Negative" : "Positive")+ " integer";
    case String s -> "String of length " + s.length();
    case null -> "Null!";
  };
}


However, this practice is not recommended: it is better to maintain the readability of the construct following common sense and leave the various clauses in the positions in which we expect to find them.

In conclusion, the order of the clauses is important, but not for the default clause and the case null clause.

Fall-Through With case null

The case null is the only case that can be used in a clause that groups multiple patterns. For example, the following clause is valid:

Java
 
case null, Integer i -> "This is a number or null";


More likely we will pair the case null with the defaultclause:

Java
 
case null, default -> "Unknown or null";


In this case, the case null must be specified before the default clause. The following code will produce a compile-time error:

Java
 
default, case null -> "Unknown or null";

 

Exhaustiveness With Sealed Types

The concept of exhaustiveness, which we have already mentioned previously, must be revised when dealing with sealed type hierarchies (sealedclasses and interfaces, see this dedicated article). Let's consider the following classes:

Java
 
public sealed abstract class OpticalDisk permits CD, DVD {
    // code omitted
}

public final class CD extends OpticalDisk {
    // code omitted
}

public final class DVD extends OpticalDisk {
    // code omitted
}


The following code compiles successfully despite not specifying the default clause:

Java
 
public class OpticalReader {
    public void insert(OpticalDisk opticalDisk) {
        switch(opticalDisk) {
            case CD cd -> playDisk(cd);
            case DVD dvd -> loadMenu(dvd);
       }
    }
    // rest of the code omitted
}


Note that we don’t need to add a default clause here. In fact, the use of the abstract sealed class OpticalDisk guarantees us that as input this switch can only accept CD and DVD objects, and therefore it is not necessary to add a default clause because all cases have already been covered.

In the case of using sealed hierarchies, it is therefore not recommended to use the default clause. In fact, its absence would allow the compiler to report you any changes to the hierarchy during the compilation phase.

For example, let's now try to modify the OpticalDisk class by adding the following BluRayclass in the permits clause:

Java
 
public sealed abstract class OpticalDisk permits CD, DVD, BluRay {
    // code omitted
}

public final BluRay implements OpticalDisk {
    // code omitted
}


If we now try to compile the OpticalReader class we will get an error:

Plain Text
 
.\OpticalReader.java:3: error: the switch statement does not cover all possible input values
        switch(opticalDisk) {
        ^


which highlights that the construct violates the exhaustiveness rule.

If instead we had also inserted the default clause, the compiler would not have reported any errors

Note that if the OpticalDisk class had not been declared abstract, we could have passed as input objects of type OpticalDisk to the switch. Consequently, we should also have added a clause for objects of type OpticalDisk to comply with the exhaustiveness. Furthermore, for the rule of dominance this clause should have been positioned as the last one to comply with the dominance rule.

The alternative would have been to add a default clause.

Compilation Improvement

Java 17 implements an improved compilation behavior to prevent any issue due to partial code compilation. If in the previous example we compile only the OpticalDisk class and the BluRay class without recompiling the OpticalReader class, then the compiler would have implicitly added a default clause to the OpticalReader switch construct, whose code will launch an IncompatibleClassChangeError.

So, in cases like this, the compiler will automatically make our code more robust.

Conclusion

In this article, we have seen how Java 17 introduced pattern matching for switch as a feature preview. This new feature increases the usability of the switch construct, updating it with new concepts such as exhaustiveness, dominance, guarded patterns, a null check clause, and improving compilation management. Pattern matching for switch, therefore, represents another step forward for the language, which is on the way to becoming more robust, complex, powerful and less verbose. In the future, we will be able to exploit the pattern matching for switch to deconstruct an object by accessing its instance variables. In particular, with a single line of code we will recognize the type of the object and access its variables. In short, the best is yet to come!

Construct (game engine) Java (programming language) Object (computer science) Operator (extension) Strings Data Types

Published at DZone with permission of Claudio De Sio Cesari. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Java String: A Complete Guide With Examples
  • Generics in Java and Their Implementation
  • Mule 3 DataWeave(1.x) Script To Resolve Wildcard Dynamically
  • High-Performance Java Serialization to Different Formats

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!