Switch Statement in Java and C#

Francesco TusaFrancesco Tusa
7 min read

The selection statements described in the previous article allow choosing and executing one among different (groups of) statements based on the evaluation of a condition, such as a boolean expression. Likewise, the switch statement allows the selection and execution of a group of statements based on the matching between an expression and different patterns.

switch (EXPRESSION) 
{ 
  case PATTERN1:  
    // statements associated with PATTERN1 

  case PATTERN2:  
    // statements associated with PATTERN2

  case PATTERN3:
    // statements associated with PATTERN3

  // more case sections [...]

  default:
   // statements executed when there is no matching 
}

According to the above syntax, after the switch keyword, the EXPRESSION to be matched is specified in parenthesis. Curly braces indicate the beginning { and the end } of the construct. Multiple sections are defined as part of the switch using the case keyword, followed by the associated pattern. The statements belonging to these sections are not enclosed in curly braces but are just enlisted after the : symbol. A switch statement executes the statements in the first case section whose pattern matches EXPRESSION.

Constant Pattern Matching

The only pattern matching supported before Java version 21 and C# version 9.0 was based on constant labels, as shown below.

public static void main(String[] args)
{
   int day = // a value is assigned 

   switch (day)
   {
      case 1:
           System.out.println("Monday");
           break;
      case 2:
           System.out.println("Tuesday");
           break;
      case 3:
           System.out.println("Wednesday");
           break;
      case 4:
           System.out.println("Thursday");
           break;
      case 5:
           System.out.println("Friday");
           break;
      case 6:
           System.out.println("Saturday");
           break;
      case 7:
           System.out.println("Sunday");
           break;
      default:
            System.out.println("Invalid day number!");
   }
}

In this example, the expression—the content of the variable day—is evaluated and matched against all the labels defined in the case sections. Then, the case section with the matching label is executed.

It should be noted that the evaluation of the expression against the case labels can be optimised to avoid going through all the defined cases. This can be done using a jump table, which is faster than the sequential evaluation in an if-else-if statement described in a previous article.

After a matching case is executed, the program will continue to execute the following case sections, even if their labels do not match the expression. This behaviour is known as fall through and, depending on the program logic, may not always be desired. Therefore, as shown in the example above, a break statement is typically used at the end of each case section to exit the switch after a case is executed.

As a result, in the above program, only one case section will be executed when the program runs. Specifically, if day is 1, the program prints “Monday” and then terminates; if day is 2, the program prints “Tuesday” and then terminates; if day is 3, the program prints “Wednesday” and then terminates (and so on for the other cases).

The above listing also defines a default section. This is an optional section whose associated statements are executed when an expression does not match any case pattern. If an expression does not match any case pattern and there is no default section, the control falls out of the switch statement. In the above example, if day does not match any of the specified cases (i.e., it is not between 1 and 7), the default case is executed, printing “Invalid day number!”.

Multiple case labels

Multiple labels can be specified at the beginning of a case section; the statements in that section will be executed if the result of the expression matches any of those labels. This is useful when you want to group several cases that should result in the same action.

public static void main(String[] args) 
{
   int day = // a value is assigned 
   switch (day) 
   {
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:
          System.out.println("Weekday");
          break;
      case 6:
      case 7:
          System.out.println("Weekend");
          break;
      default:
          System.out.println("Invalid day number!");
      }
    }
}

The above code is designed to categorise the days of the week into weekdays and weekends based on the value of day. If the value does not correspond to a valid day (1-7), it indicates an invalid input. If day is 1, 2, 3, 4, or 5, the program will print “Weekday”. The break statement is used to exit the switch after executing the code for these cases. If day is 6 or 7, the program will print “Weekend”. Again, the break statement allows the flow to fall out of the switch. If day does not match any of the specified cases (1-7), the program will print “Invalid day number!”.

Using multiple labels per case is efficient and makes the code cleaner and easier to read. Instead of writing separate blocks for each day, you group them when they share the same outcome. This reduces redundancy and improves maintainability. The output statements in the above section will be executed if the content of day matches any of the defined labels for that section, which is similar to the logical OR introduced with the boolean expressions earlier.

Relational Pattern Matching

In our previous article, we used the if-else-if statement to write a program that prints a message on the screen according to the content of an int variable temperature.

public static void main(String[] args) 
{ 
  int temperature = 15 // a value is assigned

  if (temperature > 40) 
  {
    System.out.println("Weather is hot");
  } 
  else if (temperature > 20) 
  {
    System.out.println("Weather is warm");
  } 
  else 
  {
    System.out.println("Weather is cold");
  }    
}

Newer versions of Java (21) and C# (9.0) have introduced a new way to use the switch statement to evaluate an expression against a relational pattern. This feature allows you to implement the same logic as the above if-else-if statement with a switch. However, there are some differences in the syntax of this feature in these two languages.

Switch with relational pattern matching in Java

The following code provides an example of relational pattern matching in Java.

public static void main(String[] args) 
{ 
  Integer temperature = // a value is assigned
  switch (temperature) 
  {
    case Integer t when t > 40:
      System.out.println("Weather is hot");
      break;

    case Integer t when t > 20:
      System.out.println("Weather is warm");
      break;

    default:
      System.out.println("Weather is cold");
      break;
    }
}

The execution flow is similar to the one described for constant pattern matching. However, there is a notable difference related to the usage of the boxed Integer type instead of the primitive int. This is required to perform object type matching in each case first, followed by the relational pattern matching defined after the when keyword. Also, compared to the switch example with constant pattern matching, the order of the case sections is important as they are evaluated sequentially, starting from the top one.

Pattern matching is used in this example to check the value of temperature against specified conditions defined via relational operators. If the variable temperature is an object of type Integer and its value is greater than 40, the first case is matched, and “Weather is hot” is printed. Likewise, if the variable temperature is an object of type Integer and its value is greater than 20, the second case is matched, and “Weather is warm” is printed. If temperature does not match any of the above conditions, the default case is executed, and “Weather is cold” is printed.

As mentioned, the order of the cases is important because it determines the sequence in which they are evaluated. To understand this better, let's rewrite the previous code and swap the order of the first two cases.

public static void main(String[] args) 
{ 
  Integer temperature = // a value is assigned
  switch (temperature) 
  {
    case Integer t when t > 20:
      System.out.println("Weather is warm");
      break;

    case Integer t when t > 40:
      System.out.println("Weather is hot");
      break;

    default:
      System.out.println("Weather is cold");
      break;
    }
}

When the cases are evaluated from the top, any temperature value greater than 20 will always match the first case. This includes values greater than 40, defined in the second case. As a result, all these temperature values will always match the first case, making the second case unreachable. That is a case whose pattern was either handled already by an upper case or is impossible to match. The Java compiler does not flag this scenario as an error; however, the C# .NET environment will report an error at compile time.

Switch with relational pattern matching in C

The following code implements the same application logic seen above in C#.

static void Main(string[] args) 
{ 
  int temperature = // a value is assigned
  switch (temperature) 
  {
    case > 40:
      Console.WriteLine("Weather is hot");
      break;

    case > 20:
      Console.WriteLine("Weather is warm");
      break;

    default:
      Console.WriteLine("Weather is cold");
      break;
    }
}

The implementation of relational pattern matching in C# is similar to Java. Still, the syntax is simpler as it does not require initial type matching nor using the when keyword to define the relational pattern. The same considerations about unreachable cases also apply to C#, with the compiler flagging them as an error if found in the code.

0
Subscribe to my newsletter

Read articles from Francesco Tusa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Francesco Tusa
Francesco Tusa