Primitive Types in Patterns, instanceof, and switch in Java 23
Each half-year, we get a new, fresh, and yummy version of Java. Now we get Java 23, and this article will discuss JEP-455, which introduces primitive types in patterns, instanceof, and switch. Remember, this is a preview feature.
The title of this JEP seems to explain everything, but it’s worth looking into this deeper.
Before Java 23
Let’s look into the world before this JEP. Whenever we wanted to work with switch or instancesof, we were quite limited in this fashion. For the switch, we couldn't use all of the boxed types.
Whenever you would want to write that code,
Double d = 20.0d;
switch(d){
case 20.0d -> log("double is 20");
default -> log("wrong number" + d);
}
You will receive the compilation error:
java: constant label of type double is not compatible with switch selector type java.lang.Double
That’s because there were limitations for some types. In JLS, you can read that:
“The Expression is called the selector expression. The type of the selector expression must be char, byte, short, int, or a reference type, or a compile-time error occurs.”
From this quote, we can understand that int type is ok to use in switch. That’s correct, you can create code like this:
int v = 20;
switch(v){
case 20 -> log("int is 20");
default -> log("wrong number" + v);
}
And everything will work correctly. As you may know or read from our blogpost, switch in Java 21 got a lot of upgrades, you can do something like:
Integer v = 20;
switch(v){
case Integer i when i < 20-> log("This value is too low: " + i );
case Integer i -> log("This value is perfect: " + i );
}
Looks great right? We could try something like that for primitive int.
int v = 20;
switch(v){
case int i when i < 20 -> log("This value is too low: " + i );
case int i -> log("This value is perfect: " + i );
}
We run this code, and get a compilation error:
java: unexpected type
required: class or array
found: int
Why is that? Before JEP-455, Java had limited support for switch patterns.
Switch Patterns like that were possible for Record Patterns.
record Value(int i){}
void test(){
Value v = new Value(20);
switch (v) {
case Value(int i) when i < 20 -> log("This value is too low: " + i );
case Value(int i) -> log("This value is perfect: " + i );
}
}
Work correctly. Instanceof did not work with any of the primitive types.
After Java 23 - When you enable preview-feature
Long, Float, Double, and Boolean are now allowed for a switch. So, our preview example with Double works.
Instanceof also changed their behavior. It’s possible to check if the variable is on the primary type.
From now on, when we will run code like this:
int i = 42;
if(i instanceof int){
log("i is an instance of int");
}
We will have on console "i is an instance of int". Great! What about this code?
int i = 42;
if(i instanceof byte){
log("i is an instance of byte?");
}
That’s pretty easy, right? We won’t have anything because int is not an instance of byte. Unfortunately, in this example, we will get the text “i is an instance of byte?”
Why is that?
Everything is because of casting. This simple feature of Java, is when you assign a value of one primitive data type to another type.
Do you know that casting is not safe? Let's look into this example:
int i = 42;
byte b = (byte) i;
log("b value is " + b);
The value of our b variable will be 42, right? How about now?
int i = 128;
byte b = (byte) i;
log("b value is " + b);
You probably feel that something is fishy with this question. And you are right. The value of b would be -128. That's because the range of byte type is from -128 to 127, inclusive. Here, you can learn more about the Integral Types and Values.
So there is a question, if the value will change after casting is it a correct type? From JEP we learn that the answer is no.
That’s why instanceof right now is not only checking if a type is correct for references but also checks if for primitives values you can safely cast a value.
What are the consequences of this?
Before Java 23, when you tried code like this:
Integer i = 42;
switch (i) {
case Double d -> log("i is " + d);
}
you get an error:
error: incompatible types: Integer cannot be converted to Double
But for primitives, code like this:
int i = 42;
switch (i) {
case double d -> log("i is " + d)
}
It works fine, because every int can be cast to double without any problem. When you want to try the same case with narrower types, you need to make sure that your switch will be exhaustive. Like that:
int i = 10_0000;
switch (i) {
case short v -> log("i is " + v);
default -> log("not in scope of short" + i);
}
If you want to know if casting the primitive type to others is safe, you can look into Casting Contexts in JLS.
Summary
Switch patterns become more and more useful tools in our daily work. Thanks to Project Amber, we can now work with primitives with more ease and less verbose methods.
Review by Dariusz Broda