| benf.org : other : cfr : How are java 21 patterns implemented? |
First, a little rust
let message = match maybe_digit {
Some(x) if x < 10 => process_digit(x),
Some(x) => process_other(x),
None => panic!(),
};
Yup, every language looks the same after a while! JEP 441 introduced pattern matching in switches
Here's an example (back to Java!)
static String guardWithException(Object obj) {
return switch (obj) {
case String s when s.length() > 5 -> "long string";
case String s -> "short string";
case Integer i when i / 1 > 0 -> "positive";
case Integer i -> "non-positive";
case null, default -> "other";
};
}
It's interesting to look at how this is compiled
For a start - CFR (master, as of time of writing) currently generates:
static String guardWithException(Object obj) {
Object object = obj;
int n = 0;
return switch ((Object)object) {
case String s when s.length() > 5 -> "long string";
case String s -> "short string";
case Integer i when i / 1 > 0 -> "positive";
case Integer i -> "non-positive";
default -> "other";
};
}
not too shabby! But it gets really interesting if you turn off pattern switch decoding
We end up calling a method via dynamic constants, which takes the object we've been given, and a list of possible types, and a start index, and selects the first branch we could be. The 'when' clause, if failed, advances the possible start point, and tries again.
java org.benf.cfr.reader.Main c:\code\cfr\decompilation-test\test-data\output\java_21\org\benf\cfr\tests\PatternSwitch3.class --decodepatternswitch false
static String guardWithException(Object obj) {
String string;
Object object = obj;
int n = 0;
block6: while (true) {
switch (SwitchBootstraps.typeSwitch("typeSwitch", new Object[]{String.class, String.class, Integer.class, Integer.class}, (Object)object, (int)n)) {
case 0: {
String s = (String)object;
if (s.length() <= 5) {
n = 1;
continue block6;
}
string = "long string";
break block6;
}
case 1: {
String s = (String)object;
string = "short string";
break block6;
}
case 2: {
Integer i = (Integer)object;
if (i / 1 <= 0) {
n = 3;
continue block6;
}
string = "positive";
break block6;
}
case 3: {
Integer i = (Integer)object;
string = "non-positive";
break block6;
}
default: {
string = "other";
break block6;
}
}
break;
}
return string;
}
The outer loop is really interesting! This sets (here) n, which is used as a control variable by SwitchBootstraps.typeSwitch to avoid checking earlier indices..... but it means that if you have several cases, you will call TypeSwitch once for each - from reading it, of course, the tests have to happen, but it's clear to see how these switches really are o(n).....
As discussed here and here, switching on enum (unless we trust it, in Java21+) requires some hoop jumping, in order to avoid hardcoding ordinals, or enum constants.
static String enumSwitch(Object obj) {
return switch (obj) {
case EnumTest1.FOO -> "foo";
case EnumTest1.BAR -> "bar";
case EnumTest1 e -> "other enum: " + e;
default -> "not an enum";
};
}
This is where dynamic constants come in! This can't be disabled with a simple flag in CFR currently, but the above (when condys are disabled) decompiles to
static String enumSwitch(Object obj) {
Object object;
Object object2 = obj;
Objects.requireNonNull(object2);
Object object3 = object2;
int n = 0;
switch (SwitchBootstraps.typeSwitch("typeSwitch", new Object[]{ /* dynamic constant */ (java.lang.Object)java.lang.invoke.ConstantBootstraps.invoke("invoke", new java.lang.Object[]{of(java.lang.constant.ClassDesc java.lang.String ), /* dynamic constant */ (java.lang.Object)java.lang.invoke.ConstantBootstraps.invoke("invoke", new java.lang.Object[]{of(java.lang.String ), "org.benf.cfr.tests.EnumTest1"}, v6, v7), "FOO"}, v6, v7), /* dynamic constant */ (java.lang.Object)java.lang.invoke.ConstantBootstraps.invoke("invoke", new java.lang.Object[]{of(java.lang.constant.ClassDesc java.lang.String ), /* dynamic constant */ (java.lang.Object)java.lang.invoke.ConstantBootstraps.invoke("invoke", new java.lang.Object[]{of(java.lang.String ), "org.benf.cfr.tests.EnumTest1"}, v6, v7), "BAR"}, v6, v7), EnumTest1.class}, (Object)object3, (int)n)) {
case 0: {
object = "foo";
break;
}
case 1: {
object = "bar";
break;
}
case 2: {
EnumTest1 e = (EnumTest1)((Object)object3);
object = "other enum: " + String.valueOf((Object)e);
break;
}
default: {
object = "not an enum";
}
}
return object;
}
Resolving the condys gives us
java org.benf.cfr.reader.Main output\java_21\org\benf\cfr\tests\PatternSwitch11.class --decodepatternswitch false --switchexpression false
static String enumSwitch(Object obj) {
Object object;
Object object2 = obj;
Objects.requireNonNull(object2);
Object object3 = object2;
int n = 0;
switch (SwitchBootstraps.typeSwitch("typeSwitch", new Object[]{EnumTest1.FOO, EnumTest1.BAR, EnumTest1.class}, (Object)object3, (int)n)) {
case 0: {
object = "foo";
break;
}
case 1: {
object = "bar";
break;
}
case 2: {
EnumTest1 e = (EnumTest1)((Object)object3);
object = "other enum: " + String.valueOf((Object)e);
break;
}
default: {
object = "not an enum";
}
}
return object;
}
and then finishing up, we're back to.....
java org.benf.cfr.reader.Main output\java_21\org\benf\cfr\tests\PatternSwitch11.class
static String enumSwitch(Object obj) {
Object object = obj;
Objects.requireNonNull(object);
Object object2 = object;
int n = 0;
return switch ((Object)object2) {
case EnumTest1.FOO -> "foo";
case EnumTest1.BAR -> "bar";
case EnumTest1 e -> "other enum: " + String.valueOf((Object)e);
default -> "not an enum";
};
}
.... a little verbose, but not bad!
Wow. These produce some really verbose, spaghetti code. I really struggle with these currently
| Last updated 03/2026 |