你所不知道的Java之Switch

switch(Long)的故事

作为一个java新手在学习java的过程中,机缘巧合,我写了一段这样的代码

  Long l = 0L;
  switch (l){
      ...
  }

出现了这样的错误:

T.java:5: error: incompatible types: Long cannot be converted to int
switch (l){

学习过程中总会有些不能理解的地方,而我 ^ ^ 选择百度 ^ ^

Java语言规范里是这样说的

switch works with the byte, short, char, and int primitive data types.It also works with enumerated types (discussed in Enum Types), the String class, and a few special classes that wrap certain primitive types: Character, Byte, Short, and Integer .

??? only byte,short,char,int

??? Enum,String,Character,Byte,Short,Integer

嗯?String都能支持,Long居然不支持,为什么没有Long?

不能理解,我们接着 ^ ^ 百度 ^ ^

从"20年前"的Java虚拟机规范里上找到Compile Switch这一节

里面是这样说的:

Compilation of switch statements uses the tableswitch and lookupswitch instructions.The tableswitch instruction is used when the cases of the switch can be efficiently represented as indices into a table of target offsets. The default target of the switch is used if the value of the expression of the switch falls outside the range of valid indices.

编译switch 使用两种指令 tableswitch 和 lookupswitch
当switch内的case值能被表示为一个表中的索引值时,则使用tableswitch.

接着看

The Java Virtual Machine's tableswitch and lookupswitch instructions operate only on int data. Because operations on byte, char, or short values are internally promoted to int, a switch whose expression evaluates to one of those types is compiled as though it evaluated to type int.

tableswitch 和lookupswitch只操作在int数据上,对于byte char short的操作在内部都会提升为int

原来JVM底层提供两种只支持32位大小的偏移量(刚好是int类型的大小)的switch指令 tableswitchlookupswitch .
所以在java中其实也只实现了byte, short, char, and int的switch,至于他们的包装类型以及Enum,String都是Java编译器给我们的语法糖,甚至于byte,short,char也会在运行时提升为int.

都是语法糖!!那我关心一下实现可以吧 ^ ^ 百度 ^ ^

先看看原始类型的包装类是如何实现switch的.

switch(Integer)

private void integerSwitch()
{
 Integer integerS = 0;
 switch (integerS){
  case 1:
  case 0:
  case 2:
   System.out.println(integerS);
 }
}

使用 jad 反编译后:

private void integerSwitch()
{
  Integer integer = Integer.valueOf(0);
  switch(integer.intValue()){
    case 0: // '\0'
    case 1: // '\001'
    case 2: // '\002'
      System.out.println(integer);
      break;
  }
}

嗯,果然是真的,调用了Integer.intValue()返回原始类型,

等下,怎么顺序感觉怪怪的,说好的1,0,2,居然给我排了队,

还是用JDK自带的javap,看一眼bytecode吧

0: iconst_0
1: invokestatic  #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: aload_1
6: invokevirtual #3 // Method java/lang/Integer.intValue:()I
9: tableswitch   { // 0 to 2
               0: 36
               1: 36
               2: 36
         default: 43
    }
36: getstatic     #4 // Field java/lang/System.out:Ljava/io/PrintStream;
39: aload_1
40: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
43: return

嗯,jad还是可信的,前面Java虚拟机规范提到,当switch内的case值能被表示为一个表中的索引值时,则使用tableswitch
看样子是,编译器为我们调整了顺序,似乎它更喜欢tableswitch,接着看下一个类型。

switch( String )

private void test1()
{
  String feiniao = "feiniao";
    switch(feiniao){
      case "feiniao":
        System.out.println("feiniao");
        break;
      case "FB":
        System.out.println("FB");
        break;
      case "Ea":
        System.out.println("Ea");
        break;
    }
}

使用jad 反编译后:

private void test1()
{
  String s = "feiniao";
  String s1 = s;
  byte byte0 = -1;
  switch(s1.hashCode()){
    case -972010061:
      if(s1.equals("feiniao"))
        byte0 = 0;
      break;

    case 2236:
      if(s1.equals("Ea"))
          byte0 = 2;
      else
      if(s1.equals("FB"))
          byte0 = 1;
      break;
  }
  switch(byte0){
    case 0: // '\0'
      System.out.println("feiniao");
      break;

    case 1: // '\001'
      System.out.println("FB");
      break;

    case 2: // '\002'
      System.out.println("Ea");
      break;
  }
}
  • 先来看看这个,使用了新的变量s1,防止并发操作,所造成的结果不可知.
  • 然后使用String的hashcode和一个额外的switch,将对于String的switch转换为对临时变量byte0的switch。
  • 中间还帮我们处理了"Ea"和"FB"两个字符串hashcode值相同的情况。
  • 最终转换成了一个对于变量byte0的tableswtich

哇,这糖为什么可以这么甜那!接着往下看看还有没有更甜的。

switch( Enum )

enum Em {
  A,B,C,D,E;
}
private void test2(){
   Em em = Em.A;
   switch (em){
    case A:
     System.out.println("A");
     break;
    case C:
     System.out.println("C");
     break;
    case E:
     System.out.println("E");
     break;
    default:
   }
}

使用jad 反编译后:

private void test2()
  {
    Em em = Em.A;
    static class _cls1
    {

      static final int $SwitchMap$Em[];

      static
      {
        $SwitchMap$Em = new int[Em.values().length];
        try
        {
          $SwitchMap$Em[Em.A.ordinal()] = 1;
        }
        catch(NoSuchFieldError nosuchfielderror) { }
        try
        {
          $SwitchMap$Em[Em.C.ordinal()] = 2;
        }
        catch(NoSuchFieldError nosuchfielderror1) { }
        try
        {
          $SwitchMap$Em[Em.E.ordinal()] = 3;
        }
        catch(NoSuchFieldError nosuchfielderror2) { }
      }
    }

    switch(_cls1.SwitchMap.Em[em.ordinal()])
    {
    case 1: // '\001'
      System.out.println("A");
      break;

    case 2: // '\002'
      System.out.println("C");
      break;

    case 3: // '\003'
      System.out.println("E");
      break;
    }
}

信息量有点大

  • 首先方法内多了一个类,

  • 然后目录里多了一个名 T$1的类,

  • switch的东西也不是原先的我们定义的Em,

让我缓缓,,,先用javap看一下bytecode

 0: getstatic     #9 // Field Em.A:LEm;
 3: astore_1
 4: getstatic     #10 // Field T$1.$SwitchMap$Em:[I
 7: aload_1
 8: invokevirtual #11 // Method Em.ordinal:()I
11: iaload
12: tableswitch   { // 1 to 3
               1: 40
               2: 51
               3: 62
         default: 73
    }
...

从bytecode可以看到,并没有什么_cls1类,不过可以看到一个静态常量T$1.$SwitchMap$Em,目录里的也确实多了T$1.class这个类。看看这个类是个什么鬼。

static class T$1
{

  static final int $SwitchMap$Em[];

  static
  {
    $SwitchMap$Em = new int[Em.values().length];
    try
    {
      $SwitchMap$Em[Em.A.ordinal()] = 1;
    }
    catch(NoSuchFieldError nosuchfielderror) { }
    try
    {
      $SwitchMap$Em[Em.C.ordinal()] = 2;
    }
    catch(NoSuchFieldError nosuchfielderror1) { }
    try
    {
      $SwitchMap$Em[Em.E.ordinal()] = 3;
    }
    catch(NoSuchFieldError nosuchfielderror2) { }
  }
}

好像明白了什么!

  • 假如不使用这个额外的T$1类,我所想象的代码因该长这样
//常量池里的东西先不管了
0: getstatic     #9     
3: astore_1
4: aload_1
5: invokevirtual #11 // Method Em.ordinal:()I
9: lookupswitch   { // 1 to 3
            1: 40
            3: 51
            5: 62
        default: 73
}
...

编译器废了这么大力气,就为了把lookupswitch替换成tableswitch ?

看来区别就在于 tableswitchlookupswitch

这我就很好奇了,让我着回去翻Java虚拟机规范。

tableswitch 和 lookupswitch 的区别?

Where the cases of the switch are sparse, the table representation of the tableswitch instruction becomes inefficient in terms of space. The lookupswitch instruction may be used instead. The lookupswitch instruction pairs int keys (the values of the case labels) with target offsets in a table. When a lookupswitch instruction is executed, the value of the expression of the switch is compared against the keys in the table. If one of the keys matches the value of the expression, execution continues at the associated target offset. If no key matches, execution continues at the default target.

前面提到了,当switch内的case值能被表示为一个表中的索引值时,则使用tableswitch
但是,当switch里的case值非常稀疏的时候,tableswitch的做法在空间损耗方面表现得非常糟糕,
有道理,所以lookupswitch的做法是,将case的int值和转跳的偏移量作为一对放在了一个表里,
lookupswitch被执行的时候,这switch的表达式的值和这个表里的keys逐一比较,
没有找到则使用默认值,似乎在空间上是省了,不过时间上就慢了。

我们再看一下javac的源码:

// For each case, store its label in an array.
int lo = Integer.MAX_VALUE;  // minimum label.
int hi = Integer.MIN_VALUE;  // maximum label.
int nlabels = 0;               // number of labels.
...
// Determine whether to issue a tableswitch or a lookupswitch
// instruction.
long table_space_cost = 4 + ((long) hi - lo + 1); // words
long table_time_cost = 3; // comparisons
long lookup_space_cost = 3 + 2 * (long) nlabels;
long lookup_time_cost = nlabels;
int opcode =
    nlabels > 0 &&
    table_space_cost + 3 * table_time_cost <=
    lookup_space_cost + 3 * lookup_time_cost
    ?
    tableswitch : lookupswitch;

我可以概括性的说,在最大和最小case的差值不大的且label数偏多的情况下,会选择tableswitch
当差值很大而label又数不多的情况下,会选择lookupswitch

好奇宝宝时间!

说这么多有什么用呢?作为一名重度强迫症患者加好奇宝宝,我就是想知道,编译器废了这么大劲,两者性能到底能差多少?

从上面的code可以看到tableswitch的时间复杂度是O(1),lookupswitch的时间复杂度是O(n),好现在这是我们的假设,让我们看看结果。

首先我们选择使用JMH做我们的微基准测试框架

@State(Scope.Benchmark)
public class SwitchBenchmark {
  @Param({"1", "2", "3", "4", "5", "6", "7", "8"})//类似与Junit的 Parameterized
  int n;

  @Benchmark
  public long lookupSwitch() {
    return Switch.lookupSwitch(n);
  }

  @Benchmark
  public long tableSwitch() {
    return Switch.tableSwitch(n);
  }
}
public class Switch {
  public Switch() {
  }

  public static int lookupSwitch(int var0) {
    switch(var0) {
    case 1:
      return 11;
    case 2:
      return 22;
    case 3:
      return 33;
    case 4:
      return 44;
    case 5:
      return 55;
    case 6:
      return 66;
    case 7:
      return 77;
    default:
      return -1;
    }
  }

  public static int tableSwitch(int var0) {
    switch(var0) {
    case 1:
      return 11;
    case 2:
      return 22;
    case 3:
      return 33;
    case 4:
      return 44;
    case 5:
      return 55;
    case 6:
      return 66;
    case 7:
      return 77;
    default:
      return -1;
    }
  }
}

当然我们直接java代码这样写Switch类是没用的,因为这样的lookupSwitch会被被编译器优化成tableswitch
所以我们使用jasmin

.class public nefk/Switch
.super java/lang/Object

.method public <init>()V
   aload_0
   invokenonvirtual java/lang/Object/<init>()V
   return
.end method

.method public static lookupSwitch(I)I
    .limit stack 1

    iload_0
    lookupswitch
      1 : One
      2 : Two
      3 : Three
      4 : Four
      5 : Five
      6 : Six
      7 : Seven
      default : Other

One:
    bipush 11
    ireturn
Two:
    bipush 22
    ireturn
Three:
    bipush 33
    ireturn
Four:
    bipush 44
    ireturn
Five:
    bipush 55
    ireturn
Six:
    bipush 66
    ireturn
Seven:
    bipush 77
    ireturn
Other:
    bipush -1
    ireturn
.end method

.method public static tableSwitch(I)I
    .limit stack 1

    iload_0
    tableswitch 1
      One
      Two
      Three
      Four
      Five
      Six
      Seven
      default : Other

One:
    bipush 11
    ireturn
Two:
    bipush 22
    ireturn
Three:
    bipush 33
    ireturn
Four:
    bipush 44
    ireturn
Five:
    bipush 55
    ireturn
Six:
    bipush 66
    ireturn
Seven:
    bipush 77
    ireturn
Other:
    bipush -1
    ireturn
.end method

从switch 1 到 switch 8 个迭代测试200次 吞吐量平均值是这样的

Benchmark      (n)   Mode  Cnt          Score          Error  Units
lookupSwitch    1  thrpt  200  323330446.034 ±  2445701.999  ops/s
tableSwitch     1  thrpt  200  319148199.518 ±  2598409.017  ops/s

lookupSwitch    2  thrpt  200  362462418.531 ±  2113632.446  ops/s
tableSwitch     2  thrpt  200  336827136.850 ± 13390837.980  ops/s

lookupSwitch    3  thrpt  200  358321384.210 ±  3603503.904  ops/s
tableSwitch     3  thrpt  200  357352579.969 ±  4501450.687  ops/s

lookupSwitch    4  thrpt  200  401580310.966 ±  4127000.494  ops/s
tableSwitch     4  thrpt  200  376005438.482 ± 14937683.947  ops/s

lookupSwitch    5  thrpt  200  318344827.113 ±  2553139.591  ops/s
tableSwitch     5  thrpt  200  316609397.120 ±  4390158.172  ops/s

lookupSwitch    6  thrpt  200  327729755.563 ±  3328732.069  ops/s
tableSwitch     6  thrpt  200  321749811.593 ±  2651391.425  ops/s

lookupSwitch    7  thrpt  200  361246393.893 ±  4742280.906  ops/s
tableSwitch     7  thrpt  200  358376601.529 ±  3350421.622  ops/s

lookupSwitch    8  thrpt  200  347337820.333 ±  4287812.567  ops/s
tableSwitch     8  thrpt  200  355080497.046 ±  3393523.154  ops/s

预测失败!!!

对与相同的case值tableswitchlookupswitch性能是差不多的,差不多的!

嗯,前面编译器做了那么多,原来结果都是一样的,我只想问一句,大哥你是不是觉得心有点累,

我帮你监督一下你的小弟JIT。

好像被欺骗了

我们先加上两个参数-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,nefk.Switch::*,看看JIT编译出来的汇编代码

tableswitch的汇编代码:

# {method} {0x0000000108c4c008} 'tableSwitch' '(I)I' in 'nefk/Switch'
# parm0:    rsi       = int
#           [sp+0x20]  (sp of caller)
0x0000000109a49800: sub    $0x18,%rsp
0x0000000109a49807: mov    %rbp,0x10(%rsp)  ;*synchronization entry
                                            ; - nefk.Switch::tableSwitch@-1

0x0000000109a4980c: cmp    $0x4,%esi
0x0000000109a4980f: je     0x0000000109a49865
0x0000000109a49811: cmp    $0x4,%esi
0x0000000109a49814: jg     0x0000000109a49841
0x0000000109a49816: cmp    $0x2,%esi
0x0000000109a49819: je     0x0000000109a4983a
0x0000000109a4981b: cmp    $0x2,%esi
0x0000000109a4981e: jg     0x0000000109a49833
0x0000000109a49820: cmp    $0x1,%esi
0x0000000109a49823: jne    0x0000000109a4982c  ;*tableswitch
                                            ; - nefk.Switch::tableSwitch@1lookupswitch:
...

lookupswitch的汇编代码:

# {method} {0x000000010a7b30e8} 'lookupSwitch' '(I)I' in 'nefk/Switch'
# parm0:    rsi       = int
#           [sp+0x20]  (sp of caller)
0x000000010d692200: sub    $0x18,%rsp
0x000000010d692207: mov    %rbp,0x10(%rsp)    ;*synchronization entry
                                              ; - nefk.Switch::lookupSwitch@-1

0x000000010d69220c: cmp    $0x4,%esi
0x000000010d69220f: je     0x000000010d692265
0x000000010d692211: cmp    $0x4,%esi
0x000000010d692214: jg     0x000000010d692241
0x000000010d692216: cmp    $0x2,%esi
0x000000010d692219: je     0x000000010d69223a
0x000000010d69221b: cmp    $0x2,%esi
0x000000010d69221e: jg     0x000000010d692233
0x000000010d692220: cmp    $0x1,%esi
0x000000010d692223: jne    0x000000010d69222c  ;*lookupswitch
                                               ; - nefk.Switch::lookupSwitch@1
...

lookupswitchtableswitch进过JIT优化生成的机器码,居然用得都是二分,
醉了,不是说好tableswitch的话直接使用跳表吗,感觉自己被欺骗了。

Talk is cheap. Show me the code

看样子什么都不可靠,我们还是直接翻hotspot的源码吧,在parse2.cpp文件里的create_jump_table方法内,
终于找到了我要的!

bool Parse::create_jump_tables(Node* key_val, SwitchRange* lo, SwitchRange* hi) {
...
if (num_cases < MinJumpTableSize || num_cases > MaxJumpTableSize)
  return false;
if (num_cases > (MaxJumpTableSparseness * num_range))
  return false;
...

很明显这个方法被用来创建jump_table,而关键点就出在这个MinJumpTableSize
我们hg grep -i MinJumpTableSize 搜一下 默认值

src/share/vm/opto/c2_globals.hpp:5806:  product_pd(intx, MinJumpTableSize,                                        \
src/cpu/sparc/vm/c2_globals_sparc.hpp:5763:define_pd_global(intx, MinJumpTableSize,             5);
src/cpu/x86/vm/c2_globals_x86.hpp:5763:define_pd_global(intx, MinJumpTableSize,             10);
src/share/vm/adlc/output_h.cpp:5763:      fprintf(fp,"  %sNode() : _index2label(MinJumpTableSize*2) { ", instr->_ident);
src/share/vm/opto/parse2.cpp:5763:  if (num_cases < MinJumpTableSize || num_cases > MaxJumpTableSize)

可以看到在sparc下是5,在x86下是10,因为我们的case小于10个,所以跟本没有生成jump_table

结论

不论btyecode所用使用的swtich是tableswitch还是lookupswitch,JIT会在运行时根据case数量和配置的参数,作出优化。

  • 默认 MinJumpTableSize 为 10 ,case的数量小于这个不会生成jump_table

  • 默认 MaxJumpTableSize 为 65000 ,case的数量大于这个不会生成jump_table

  • 默认 MaxJumpTableSparseness 为 5 ,case过于稀疏不会生成jump_table

  • 最重要的一点是,大多数情况下源码都是最好的文档(除去那些无法直视代码)。

  • Talk is cheap. Show me the code!

  • Talk is cheap. Show me the code!!

  • Talk is cheap. Show me the code!!!

环境信息

  • Mac OS
  • JDK 1.8.0_151

感谢Lydia觉醒的宝贵建议和辛苦校对。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容