primitive types in patterns, instanceof, and switch - feature imageprimitive types in patterns, instanceof, and switch - feature image
HappyCoders Glasses

Primitive Types in Patterns, instanceof, and switch

Sven Woltmann
Sven Woltmann
Last update: December 4, 2024

In this article, you will learn:

  • What is Pattern Matching?
  • How can we use primitive types in pattern matching with instanceof?
  • How can we use primitive types in pattern matching with switch?
  • What is the difference between pattern matching with primitive types and object types (“reference types”)?
  • What are dominating and dominated primitive types?

We begin with a brief refresher on pattern matching in Java. If you’re already familiar with pattern matching in general, feel free to skip the introductory chapter and go directly to the second chapter, Changes in Java 23.

What is Pattern Matching?

Pattern Matching in Java was first released as a final feature in Java 16 with Pattern Matching for instanceof, and expanded in Java 21 with Pattern Matching for switch.

The following code example uses pattern matching to determine if the variable obj is of type String, and if so, converts it to uppercase and prints it:

Object obj = . . .
if (obj instanceof String s) {
  System.out.println(s.toUpperCase());
}Code language: Java (java)

The pattern in this example is String s. The code first checks if the Object variable obj matches this pattern. It does if obj is of type String. If that’s the case, the content of obj is made available in the String variable s, converted to uppercase, and printed.

The following example is a bit more complex and uses switch instead of instanceof to match the variable obj against different patterns and perform different actions depending on the type:

switch (obj) {
  case String s when s.length() >= 5 -> System.out.println(s.toUpperCase());
  case Integer i                     -> System.out.println(i * i);
  case Number n                      -> System.out.println(n + " is a number");
  case null, default                 -> System.out.println(obj);
}Code language: Java (java)

The first pattern, String s when s.length() >= 5, is a so-called “guarded pattern,” a pattern with a restriction, and s.length() >= 5 is the “guard.” The variable obj matches this pattern if it is of type String and this string is at least five characters long.

The second pattern, Integer i, matches if obj is of type Integer.

The third pattern, Number n, matches if obj is of a type extending the abstract class Number, for example, Long or Double, but also AtomicInteger or BigDecimal. The pattern would also match variables of type Integer, but those are already “intercepted” in the previous line by the pattern Integer i.

Changes in Java 23

So far, pattern matching only works with reference types, such as String and Integer, but not with primitive types like int, long, and double.

In Java 23, the preview feature "Primitive Types in Patterns, instanceof, and switch" was introduced through JDK Enhancement Proposal 455, and in Java 24, the feature is being re-proposed without changes through JEP 488.

When you activate this feature with --enable-preview, you can:

  1. use primitive types in pattern matching,
  2. use constants of types long, float, double, and boolean in switch.

I’ll describe the first change in detail in the upcoming section, Primitive Types in Pattern Matching. I’ll quickly explain the second change here:

For a long time, we’ve been able to compare a variable with constants using switch, for example like this:

int code = . . .
switch (code) {
  case 200 -> System.out.println("OK");
  case 400 -> System.out.println("Bad Request");
  case 404 -> System.out.println("Not Found");
  . . .
}Code language: Java (java)

However, this has so far only worked with the types byte, short, char, and int. If you replace int in the first line with long, for example, you’ll get a compiler error:

error: selector type long is not allowedCode language: plaintext (plaintext)

If you activate “Primitive Types in Patterns, instanceof, and switch” with --enable-preview in Java 23 or 24, this error message disappears. You can then use any primitive type in switch.

Primitive Types in Pattern Matching

An object matches a pattern if the object can be assigned to a variable of the pattern’s type. As you saw in the previous section, for example, an Integer object matches the pattern Integer i – but it would also match the patterns Number n or Object o – and even Comparable c or Serializable s – because Integer extends Number and implements Comparable, among others, and Number extends Object and implements Serializable:

Class diagram: Integer extends Number, Number extends Object

However, with primitive types, there is no inheritance. Therefore, pattern matching with primitive types doesn't work exactly like with reference types – but similarly.

In the following section, I will explain how primitive types can be used in pattern matching with instanceof. In the subsequent section, I will then show you primitive types in pattern matching with switch.

Primitive Type Patterns with instanceof

Don’t be alarmed: I will start with a mathematically sounding formulation but then immediately explain with an example what I mean.

Be a a variable of a primitive type (i.e., byte, short, int, long, float, double, char, or boolean) and B one of these primitive types. Then a instanceof B results in true if the precise value of a can be stored in a variable of type B.

Example 1

Here comes the example:

int value = . . .
if (value instanceof byte b) {
  System.out.println("b = " + b);
}Code language: Java (java)

The code is to be read as follows: If the value of the variable value can also be stored in a byte variable, then assign this value to the byte variable b and print it.

For value = 5, this would be the case; for value = 1000, however, not, since a variable of type byte can only store values from -128 to 127.

Example 2

Here is a second example:

double value = . . .
if (value instanceof float f) {
  System.out.println("f = " + f);
}Code language: Java (java)

Here, we check whether the double value can also be represented as a float. This would be the case for value = 1.5 but not for value = Math.PI, since float is not precise enough to capture all digits of the double constant Math.PI.

Example 3

Let’s assign a specific value to value and check it against all numeric primitive types (a comparison of numeric types with boolean is not allowed and leads to a compiler error).

Here is, instead of a code snippet, a complete, executable demo program:

void main() {
  int value = 65;
  if (value instanceof byte b)   System.out.println(value + " instanceof byte:   " + b);
  if (value instanceof short s)  System.out.println(value + " instanceof short:  " + s);
  if (value instanceof int i)    System.out.println(value + " instanceof int:    " + i);
  if (value instanceof long l)   System.out.println(value + " instanceof long:   " + l);
  if (value instanceof float f)  System.out.println(value + " instanceof float:  " + f);
  if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
  if (value instanceof char c)   System.out.println(value + " instanceof char:   " + c);
}Code language: Java (java)

If you save the program, for example, in the file Test.java, you can start it in Java 23 and 24 as follows:

java --enable-preview Test.javaCode language: plaintext (plaintext)

You will then see the following output:

65 instanceof byte:   65
65 instanceof short:  65
65 instanceof int:    65
65 instanceof long:   65
65 instanceof float:  65.0
65 instanceof double: 65.0
65 instanceof char:   ACode language: plaintext (plaintext)

The value 65 can, therefore, be stored in variables of all other primitive types (except boolean). You can see that as float and double, this value is displayed with one decimal place and as char as the character ‘A’ (whose ASCII code is 65).

Example 4

If we set value to 100,000, we get the following output:

100000 instanceof int:    100000
100000 instanceof long:   100000
100000 instanceof float:  100000.0
100000 instanceof double: 100000.0Code language: plaintext (plaintext)

The value 100,000 can, therefore, be stored in variables of type int, long, float, and double but not in variables of type byte, short, and char. Their number range only goes up to 127, 32,767 and 65,535.

Example 5

For value = 16_777_217, things get interesting:

16777217 instanceof int:    16777217
16777217 instanceof long:   16777217
16777217 instanceof double: 1.6777217E7Code language: plaintext (plaintext)

So the number 16,777,217 can be stored in int, long, and double, but not in float?

That is indeed the case! Run the following code:

float f = 16_777_217;
System.out.printf("f = %.1f%n", f);Code language: Java (java)

The result is unexpected:

f = 16777216.0Code language: plaintext (plaintext)

The printed number ends in 6, not 7!

That is because the floating point type float has limited accuracy and can store, for example, 16.777.216, 16.777.218, and 16.777.220, but not the values 16.777.217 and 16.777.219 in between.

Example 6

In the following example, value is a floating-point number of type float:

void main() {
  float value = 3.5f;
  if (value instanceof byte b)   System.out.println(value + " instanceof byte:   " + b);
  if (value instanceof short s)  System.out.println(value + " instanceof short:  " + s);
  if (value instanceof int i)    System.out.println(value + " instanceof int:    " + i);
  if (value instanceof long l)   System.out.println(value + " instanceof long:   " + l);
  if (value instanceof float f)  System.out.println(value + " instanceof float:  " + f);
  if (value instanceof double d) System.out.println(value + " instanceof double: " + d);
  if (value instanceof char c)   System.out.println(value + " instanceof char:   " + c);
}Code language: Java (java)

Now, the program prints the following:

3.5 instanceof float:  3.5
3.5 instanceof double: 3.5Code language: plaintext (plaintext)

Of course, a number with decimal places can only be displayed using float and double.

Example 7

However, if we set value to 100000.0f, the result is as follows:

100000.0 instanceof int:    100000
100000.0 instanceof long:   100000
100000.0 instanceof float:  100000.0
100000.0 instanceof double: 100000.0Code language: plaintext (plaintext)

The floating point number 100,000.0 can also be stored in an int or a long as it has no decimal places.

Pattern Matching with boolean

boolean, by the way, can only be compared with boolean. Any comparison of boolean with another type or another type with boolean will lead to an “incompatible types” compiler error.

Pattern matching with boolean isn’t very useful anyway because matching a boolean variable against the type boolean always results in true.

Primitive Type Patterns with instanceof and &&

Just like with reference types, you can also directly append further checks in the instanceof check with && for primitive types. The following code, for example, only prints positive byte values (i.e., 1 to 127):

int a = . . .
if (a instanceof byte b && b > 0) {
  System.out.println("b = " + b);
}Code language: Java (java)

Primitive Type Pattern with switch

We can use primitive patterns not only with instanceof but also with switch:

void main() {
  double value = 100000.0;
  switch (value) {
    case byte   b -> System.out.println(value + " instanceof byte:   " + b);
    case short  s -> System.out.println(value + " instanceof short:  " + s);
    case char   c -> System.out.println(value + " instanceof char:   " + c);
    case int    i -> System.out.println(value + " instanceof int:    " + i);
    case long   l -> System.out.println(value + " instanceof long:   " + l);
    case float  f -> System.out.println(value + " instanceof float:  " + f);
    case double d -> System.out.println(value + " instanceof double: " + d);
  }
}Code language: Java (java)

The program produces the following output:

100000.0 instanceof int:    100000Code language: plaintext (plaintext)

Here, we do not see all matching patterns, but only the first one, because only a single program path is executed through switch.

Here are a few examples for value along with the type of the first matching pattern:

valueFirst matching typeNumber range of the matching type
0byte-128 to 127
10,000short-32,768 to 32,767
50,000char0 to 65,535
1,000,000int-2,147,483,648 to 2,147,483,647
1,000,000,000,000longapprox. minus to plus 9 trillion
0.125floatSingle-precision floating-point numbers
0.126doubleDouble-precision floating-point numbers

Primitive Type Patterns with switch and when (“Guarded Pattern”)

With primitive type patterns in switch, we can also use “guards,” i.e., narrow the pattern using when and a boolean expression. This can be helpful, for example, when we want to group by number ranges, such as HTTP status codes.

Here is an example that was previously only possible with an if-else chain:

private String getHttpStatusMessage(int code) {
  if (code == 200) return "OK";
  else if (code == 400) return "Bad request";
  else if (code == 404) return "Not found";
  else if (code == 500) return "Internal server error";
  else if (code > 100 && code < 200) return "Informational";
  else if (code > 200 && code < 300) return "Success";
  else if (code > 302 && code < 400) return "Redirection";
  else if (code > 400 && code < 500) return "Client error";
  else if (code > 502 && code < 600) return "Server error";
  else return "Unknown code";
}Code language: Java (java)

In the future, we can write this method – in my opinion, much more concisely – using switch as follows:

private String getHttpStatusMessage(int code) {
  return switch (code) {
    case 200 -> "OK";
    case 400 -> "Bad request";
    case 404 -> "Not found";
    case 500 -> "Internal server error";

    case int i when i > 100 && i < 200 -> "Informational";
    case int i when i > 200 && i < 300 -> "Success";
    case int i when i > 302 && i < 400 -> "Redirection";
    case int i when i > 400 && i < 500 -> "Client error";
    case int i when i > 502 && i < 600 -> "Server error";

    default -> "Unknown code";
  };
}Code language: Java (java)

Dominant and Dominated Primitive Types

For switch with primitive types, we must observe the principle of dominating and dominated types – just as with object types.

A dominant type is one that can represent all values of a dominated type.

For example, byte is dominated by int, as each byte value can also be stored as an int. Take a look at the following code.

In the following examples, I have used the unnamed variable _ (underscore), finalized in Java 22.

double value = . . .
switch (value) {
  case int    _ -> System.out.println(value + " instanceof int");    // dominating type
  case byte   _ -> System.out.println(value + " instanceof byte");   // dominated type
  case double _ -> System.out.println(value + " instanceof double");
}Code language: Java (java)

The case byte label would never match here, as every byte is also an int and would, therefore, be intercepted by the case int label.

If you were to try to compile this code, it would lead to the following compiler error:

 error: this case label is dominated by a preceding case label
    case byte   _ -> System.out.println(value + " instanceof byte");
         ^Code language: plaintext (plaintext)

Generally, a dominated type must always be listed before a dominant type. So the following is OK:

double value = . . .
switch (value) {
  case byte   _ -> System.out.println(value + " instanceof byte");   // dominated type
  case int    _ -> System.out.println(value + " instanceof int");    // dominating type
  case double _ -> System.out.println(value + " instanceof double");
}Code language: Java (java)

Exhaustiveness Analysis for switch

For all new switch features (i.e., all those added since Java 21), the switch must be exhaustive, meaning there must be a matching case label for every possible value of the selector expression (in the example, the variable value).

That’s why the previous examples also included a case double label. The following would not be permitted:

double value = . . .
switch (value) {
  case byte   _ -> System.out.println(value + " instanceof byte");
  case int    _ -> System.out.println(value + " instanceof int");
}Code language: Java (java)

This switch is incomplete and therefore invalid; for example, no case label would match the value 3.5. The compiler would produce the following error:

error: the switch statement does not cover all possible input values
  switch (value) {
  ^Code language: plaintext (plaintext)

The following switch, on the other hand, is complete:

short value = . . .
switch (value) {
  case byte   _ -> System.out.println(value + " instanceof byte");
  case int    _ -> System.out.println(value + " instanceof int");
}Code language: Java (java)

Although there is no case short label here, there is a case int label, and every possible short value matches against it.

Summary

With the option --enable-preview, you can activate the feature “Primitive Types in Patterns, instanceof, and switch” in Java 23 and 24. This allows you to match against primitive type patterns like int i or double d with instanceof and switch.

Since there is no inheritance for primitive types, primitive patterns work somewhat differently than patterns with reference types: a variable matches a primitive pattern if a variable of the target type can accommodate it without loss of precision.

Do you want to always stay up-to-date and be informed as soon as a new article is published on HappyCoders.eu? Then click here to sign up for the HappyCoders newsletter.