4

How does the new Java 17 type pattern matching switch works under the hood ? As the feature is fairly new, this question doesn't talk about it.

Reminder: for this code to work under Java 17, you need to enable preview features

public static void test (Object o) {
  System.out.println(switch(o){
    case Number n -> "number " + n;
    case Enum e -> "enum " + e;
    case String s -> "string " + s;
    default -> "other " + o;
  });
}

Disassembled version of the code above using javap -c:

   0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
   3: aload_0
   4: dup
   5: invokestatic  #13                 // Method java/util/Objects.requireNonNull:(Ljava/lang/Object;)Ljava/lang/Object;
   8: pop
   9: astore_1
  10: iconst_0
  11: istore_2
  12: aload_1
  13: iload_2
  14: invokedynamic #19,  0             // InvokeDynamic #0:typeSwitch:(Ljava/lang/Object;I)I
  19: tableswitch   { // 0 to 2
                 0: 44
                 1: 58
                 2: 74
           default: 90
      }
  44: aload_1
  45: checkcast     #23                 // class java/lang/Number
  48: astore_3
  49: aload_3
  50: invokedynamic #25,  0             // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/Number;)Ljava/lang/String;
  55: goto          96
  58: aload_1
  59: checkcast     #29                 // class java/lang/Enum
  62: astore        4
  64: aload         4
  66: invokedynamic #31,  0             // InvokeDynamic #2:makeConcatWithConstants:(Ljava/lang/Enum;)Ljava/lang/String;
  71: goto          96
  74: aload_1
  75: checkcast     #34                 // class java/lang/String
  78: astore        5
  80: aload         5
  82: invokedynamic #36,  0             // InvokeDynamic #3:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
  87: goto          96
  90: aload_0
  91: invokedynamic #39,  0             // InvokeDynamic #4:makeConcatWithConstants:(Ljava/lang/Object;)Ljava/lang/String;
  96: invokevirtual #42                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
  99: return

Given this key line:

  14: invokedynamic #19,  0             // InvokeDynamic #0:typeSwitch:(Ljava/lang/Object;I)I

It looks like Java converts the object into an int in an auto-generated int typeSwitch (Object, int) method, of which I can't see the generated code. The goal of this conversion is then to be able to use a regular switch table over int values.

At first, I thought that hashCode of was used, i.e. object.getClass().hashCode(), as it is done with switch over String, but in fact Class object's hash code isn't constant over consecutive runs of the JVM. So it's impossible unless the mapping table is rebuilt at every execution, what I doub about.

So, the questions are:

What does this typeSwitch method do ?

Is it smarter than a chain of if instanceof else if instanceof ? Is that possible ?

What's the purpose of the additional int passed ?

QuentinC
  • 12,311
  • 4
  • 24
  • 37
  • You can see the bootstrap method by running `javap -c -v`. It calls [SwitchBootstraps.typeSwitch()](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/runtime/SwitchBootstraps.html#typeSwitch(java.lang.invoke.MethodHandles.Lookup,java.lang.String,java.lang.invoke.MethodType,java.lang.Object...)) –  Dec 15 '21 at 06:01
  • I've updated my answer with information about the additional int parameter – Thomas Kläger Dec 17 '21 at 10:28

1 Answers1

5

What does this typeSwitch() method do?

The invokeDynamic instruction (upon first being hit) calls the SwitchBootstraps.typeSwitch() method. This method then returns what method call should be executed (this is what invokedynamic generally does).

The last argument of the SwitchBootstraps.typeSwitch() method (the labels parameter) is in this case the list of classes in the switch: Number.class, Enum.class, String.class

The SwitchBootstraps.typeSwitch() bootstrap method checks the labels parameter for correctness and then returns a ConstantCallSite for the SwitchBootstraps.doTypeSwitch() method that does the effective handling (i.e. the final execution of the invokeDynamic instruction).

If you look at what SwitchBootstraps.doTypeSwitch() does: it iterates over the list of classes and returns the first found match.

What's the purpose of the additional int passed?

The additional parameter (startIndex) is needed because of this case:

public static void test(Object o) {
    System.out.println(switch(o){
        case Number n && n.intValue() >= 10 -> "large number " + n;
        case Number n -> "small number "+ n;
        case Enum e -> "enum " + e;
        case String s -> "string " + s;
        default -> "other " + o;
    });
}

There are now two cases that match the class java.lang.Number and the first case is guarded by the condition n >= 10.

If I call test(5) the doTypeSwitch() method is called with the object to test (Integer.valueOf(5)), a startIndex of 0 and a list of classes. It iterates over the list of classes and stops as soon as it finds the first Number entry and returns its index (0).

It starts processing the first case and checks the guard. Since I called it with the value 5 the guard fails and the code loops back to the doTypeSwitch() call. However this time it is called with object to test (Integer.valueOf(5)), a startIndex of 1 and the same list of classes.

Is the code smarter (or faster) than a list of if (o instanceof Number n && n >= 10) {} else if (n instanceof Number) {}?

In its current state certainly not faster. But remember that this is a preview feature in its first iteration (https://openjdk.java.net/jeps/406): the Java Architects are experimenting with this feature and obtaining optimal performance of typeSwitch is probably not the first concern.

There is already a second iteration for this feature (https://openjdk.java.net/jeps/420) and future releases might improve the performance once the design has settled.

But even then: to implement the test(Object o) method with an if / else if chain you would have to split the method into two:

public static void test2(Object o) {
    System.out.println(check2(o));
}

private static String check2(Object o) {
    if (o instanceof Number n && n.intValue() >= 10)
        return "large number " + n;
    else if (o instanceof Number n)
        return "small number "+ n;
    else if (o instanceof Enum e)
        return "enum " + e;
    else if (o instanceof String s)
        return "string " + s;
    else
        return "other " + o;
}

which about doubles the line count.

Is that (making it smarter) possible ?

That is at least conceivable. The class labels could be rearranged, they could be made a tree structure depending on the class hierarchy of the class labels or do some other smart things at runtime depending upon the most commonly used path.

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34