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:
- use primitive types in pattern matching,
- use constants of types
long
,float
,double
, andboolean
inswitch
.
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 allowed
Code 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
:
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.java
Code 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: A
Code 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.0
Code 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.6777217E7
Code 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.0
Code 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.5
Code 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.0
Code 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: 100000
Code 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:
value | First matching type | Number range of the matching type |
---|---|---|
0 | byte | -128 to 127 |
10,000 | short | -32,768 to 32,767 |
50,000 | char | 0 to 65,535 |
1,000,000 | int | -2,147,483,648 to 2,147,483,647 |
1,000,000,000,000 | long | approx. minus to plus 9 trillion |
0.125 | float | Single-precision floating-point numbers |
0.126 | double | Double-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.
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.