Note: this article is written before Java 21 was released. In Java 21 and newer, the bootstrap method works slightly differently, while the general idea is similar.
Pattern matching for switch statements and expressions has evolved as the latest releases unveiled. We can already play with the most exciting changes, such as pattern guards and record patterns, in the Java 19 and 20 preview versions.
Let’s consider the following example:
We can now have multiple cases with the same type but with added guard conditions. Also, the records can be decomposed, giving us access to any nested parameters immediately. Pretty cool, isn’t it?
But I’m not here to tell you how to use the latest pattern matching. Instead, I invite you to delve beneath this beautiful syntax and explore how it works internally at the bytecode level.
bytecode preparations for the switch statement
The full bytecode output for this class is huge, so let me show you the most interesting bits.
This is how the Code
section of the test method starts:
Let’s go step by step and see what’s happening here.
Since our test method takes an Object
argument, the Object
reference is initially stored in the local
variable array at index 0
. The instruction aload_0
is used to load the Object reference at index 0
onto
the operand stack.
dup
instructs to duplicate the top value of the stack:
This instruction invokes a static method requireNonNull
residing in java.util.Objects
.
The method takes an Object
as an argument and returns an Object
. It checks if the value is not
null and returns it; otherwise, it throws an exception.
The status of our local variable array and operand stack is the following:
pop
instructs to remove the top value from the operand stack:
This operation stores the value from the top of the operand stack as a local variable at index 1
:
Load a constant integer value 0
onto the stack:
Store the top integer value from the stack as a local variable:
Load an Object reference from local variables at index 1
onto the operand stack:
Load an integer value from local variables at index 2
onto the operand stack:
The next instructions are invokedynamic
and tableswitch
, and they deserve a deeper explanation.
invokedynamic
and tableswitch
teamwork
In this particular case, the invokedynamic
and tableswitch
instructions work together as a team.
As the name suggests, invokedynamic
invokes a dynamically determined method, meaning that we “don’t know”
which method it will be at compile time.
Why is there a need to dynamically determine how exactly the switch statement should work? One idea behind this approach is to separate as much of the switch logic as possible from our bytecode, making it more compact. This logic, including type determination, is implemented in the bootstrap methods.
Another benefit of this approach is that if a newer Java version introduces more advanced logic for switch statements, our previously generated bytecode will continue to work without recompiling and can also benefit from the new features.
It’s supposed to work quickly since we only go through the method lookup logic the first time. After that, the target method is linked directly, and under the same circumstances, the JVM will invoke the linked method as any normal one.
The invokedynamic
instruction is used for many bits of the Java functionality, including String concatenation,
lambdas, etc.
Specifically for the switch functionality, invokedynamic
is used along with one of the two bootstrap methods:
typeSwitch
or enumSwitch
. They facilitate the invocation of doTypeSwitch
or doEnumSwitch
methods, respectively.
In our example, we are not dealing with enum types. However, our switch statement contains a mix of different
types, which is the doTypeSwitch
territory. The doTypeSwitch
takes the Object and int values from the operand stack and returns an int.
This integer value is then used by the tableswitch
or lookupswitch
instructions to determine
the target address and jump to that location.
invokedynamic
for type switch
Let’s take a closer look at the invokedynamic
instruction in our bytecode:
The hint provided by javap
indicates that the reference #39
in the constant pool points to the value:
InvokeDynamic #0:typeSwitch:(Ljava/lang/Object;I)I
, which contains another reference #0
.
That reference points to an interesting section in our bytecode called BootstrapMethods
. Let’s see its entry #0
,
since it’s the relevant one for us:
Entry #0
points to the typeSwitch
method located in the java.lang.runtime.SwitchBootstraps
class.
This is the bootstrap method that will be invoked for our switch statement.
the typeSwitch
bootstrap method
All right, what is so special about this bootstrap method?
It has to dynamically (at runtime) determine and prepare the target method for invocation.
Let’s check the source code:
The bootstrap method accepts an instance of Lookup
, which is the lookup context.
JVM places the lookup context onto the operand stack before it invokes the method.
invocationName
holds the name of the invoke method that actually performs the invocation.
However, that value is not even used.
The MethodType
holds the descriptor of the target method. This value is taken from a specific NameAndType
entry in the constant pool, which is referenced for invokedynamic
instruction.
These are the referenced entries:
Here, the MethodType
is expressed through the following descriptor: (Ljava/lang/Object;I)I
,
which means that the argument types are Object
and int
and the return type is int
.
The most interesting argument of the typeSwitch
method is Object… labels
. It is an array of all types
for the cases in our switch statement. For us, these types are: String
, String
, String
, Test
and ParentTest
.
The names of these types reside in the constant pool as Class
entries. We pass references to these
types as arguments to the bootstrap method in the BootstrapMethods
section:
Now, the typeSwitch
method has all the information it requires.
What does it do exactly?
It verifies the arguments, then creates an instance of CallSite
and puts it onto the stack.
The call site contains a MethodHandle
, which is the target method to be called the final goal of
invokedynamic
instruction.
How does it obtain that super important MethodHandle
? There’s a special mechanism that can be considered
a more modern, safer, and faster kind of reflection. It is used to look up the MethodHandle
instance.
In the bootstrapping code for switch cases, we see that we are looking for a specific MethodHandle DO_TYPE_SWITCH
,
which is initialized in a static block:
We see here that the target method’s name is “doTypeSwitch”
. Surprisingly, it accepts more arguments
than we expected. Why? The first two arguments are taken normally from the operand stack, and the last
argument is injected by the bootstrap method:
As you remember, we accepted labels (collection of case types) as the bootstrap method argument, now we inject it as the second argument for our target method.
Finally, the invokeExact
method is going to use the CallSite
previously placed onto the stack to
invoke the target method.
the target doTypeSwitch
method
Here is the source code of the target doTypeSwitch
method:
In the beginning, we have pushed the following values onto the stack: a reference to an Object
for which
the entire switch statement is being executed, and an integer value initialized to 0
.
If the first one is clear, then what is that integer value for?
Let’s look at our switch statement again:
We have three cases where the type is String
. Let’s assume that the incoming Object
is indeed an
instance of String
. The mysterious integer value (let’s call it tmp
) at this moment is equal to 0
.
So, what happens next?
We reach the invokedynamic
instruction, which looks up and calls the doTypeSwitch
method.
The result of this method is the integer value sitting at the operand stack.
This value allows us to determine the address and jump to a specific bytecode instruction.
Only after the jump will we evaluate the guard condition s.length() == 1
. If this condition is not satisfied,
we overwrite the value of tmp
to 1
and go back to the start. Once there, we push it again onto the
operand stack and invoke the doTypeSwitch
method again, where we accept startIndex
equal to 1
this time.
When we hit the doTypeSwitch
method for the second time, we once again end up in a String case.
However, the different startIndex
will result in a different return value. That will bring us to a
different bytecode address. We will jump to a second branch, where we assess the guard condition for that
branch: when s.length() == 2
.
We will continue this process until we satisfy a guard condition or encounter a section of code that does not have a guard condition. We can exit the switch statement once we execute a code block for one of the cases.
Let’s check the part of the bytecode that illustrates this logic (see my comments in bold):
dynamic enum switch
As mentioned earlier, there is another dynamically bootstrapped method for switch statements: doEnumSwitch
.
This method is responsible for handling cases similar to the following:
This example contains the enum type and the specific enum values in the same switch statement.
The bootstrapping process is similar. In the BootstrapMethods
section, we will see the enumSwitch
method that will facilitate the invocation of doEnumSwitch
.
tableswitch
vs lookupswitch
Let’s slightly change our example:
If we compile it and check the bytecode, we will find lookupswitch instead of tableswitch:
What is the difference between these two?
tableswitch
holds the table of bytecode addresses, where we can get any address by index,
like in an array, which is super fast. The complexity is O(1)
.
The lookupswitch
, on the other hand, holds the mapping of keys to addresses.
The keys in the table must always be sorted. When searching for a specific address, it performs a
binary search through the keys. This operation is slightly slower but still quite fast,
with a complexity of O(log n)
.
What is the benefit of using the lookupswitch
then?
Well, in our case, the reason is the size of this construction. We have only two entries in the table,
which takes up less space compared to the minimum space required for the tableswitch
.
So the speed benefit of the tableswitch
is not worth the extra space that can be saved.
However, there are more fun cases in nature where lookupswitch
becomes much more sensible than tableswitch
.
Let’s consider the following example:
This will result in a tableswitch
:
The integer value itself is used as a key to find a bytecode address in the tableswitch
.
Since all values from 1
to 4
are covered in a switch statement, they become excellent indices.
Let’s change the code to the following:
In this case, to use a tableswitch
, we need to cover all indices between 1
and 10
, 10
and 100
,
and so on in that table. This would result in a large structure, making it impractical to use a tableswitch
.
Instead, we look up the address using lookupswitch
, where we compare our integer value to the keys to find
the correct address.
The bytecode transforms to the following:
In summary, lookupswitch
is more suitable for cases where the values are more scattered, with gaps between the keys.
Last Thoughts
Dynamic method invocation was not originally supported in the JVM.
Initially, the JVM was designed with a single statically typed language in mind — Java. However, generating the class files and letting the JVM run them was so convenient that more and more JVM languages started emerging.
Dynamically-typed languages had a hard time because in the JVM, everything had to be known at compile time.
So, the invokedynamic
instruction was introduced to support dynamically typed languages, too*.
It proved to be useful for Java as well. For example, it helps ensure that certain classes or methods are generated at runtime only when necessary (like for lambdas). If the lambda function is never executed at runtime, we save space with a more compact bytecode.
The only advantage of dynamic invocation for switch statements is the separation of the type-checking
logic away from our bytecode. However, let’s highlight this again: when we see a typeSwitch
method in our
bytecode, we already know precisely which method it will bootstrap (because currently, there is only one
option: doTypeSwitch
). The same applies to enumSwitch
, which exclusively bootstraps the doEnumSwitch
method.
So, why can’t it be a normal invocation of a method known at compile time accepting an array of Strings with the case types? Perhaps it’s related to the work required in the bytecode to load all the necessary parameters onto the operand stack before calling such a method. Another possible reason is that there are plans to introduce more options that could be dynamically selected.
Another mystery is related to the benefits of using the dynamic enum switch.
I did some basic performance comparisons for the new syntax that uses the enum switch and the old way of reaching the same result without dynamic invocation.
With dynamic invocation:
Without dynamic invocation:
I couldn’t prove that the new way for this particular case gives a performance benefit. In fact, it seems to work a little slower (which is not unexpected), and the bytecode takes up more space.
So, I’m looking forward to seeing what will change around this functionality in the upcoming releases.
Stay tuned 😎.
- I learned the JVM was initially designed exclusively for Java from Edoardo’s talk “WebAssembly for the Java Geek” at JNation 2023. Check out this paper.