深入理解java之关于switch的探究

switch是Java条件语句语法之一。在多条件下相对于使用 if/else,使用switch更为简洁。语法是:

switch(表达式){
    case 值1: 代码1;break;
    case 值2: 代码2;break;
    ...
    case 值n:代码n;break;
    default:代码n+1
}

switch是根据表达式的值不同来执行不同的分支,具体来说,根据表达式的值找匹配的case,然后执行后面的代码,碰到break时结束,如果没有找到匹配的值则执行default都的语句。

需要注意的是:

  • 表达式值得数据类型只能是byteshortintchar枚举String(java7)。
  • 在switch语句中,表达式的值不能是null,否则会在运行时抛出NullPointerException。
  • 在case子句中也不能使用null,否则会出现编译错误
  • case子句的值不能相同,也会编译不通过。

首先提问:switch是怎么实现的呢?

想要了解switch的实现原理,那先从条件语句执行的实现说起。序最终都是一条条的指令,CPU有一个指令指示器,指向下一条要执行的指令,CPU根据指示器的指示加载指令并且执行。指令大部分是具体的操作和运算,在执行这些操作时,执行完一个操作后,指令指示器会自动指向挨着的下一条指令。
但有一些特殊的指令,称为跳转指令,这些指令会修改指令指示器的值,让CPU跳到一个指定的地方执行。跳转有两种:一种是条件跳转;另一种是无条件跳转。条件跳转检查某个条件,满足则进行跳转,无条件跳转则是直接进行跳转。

如下面if/else的代码,实际上就会转换为这些跳转指令。

1.int a= 10;
2.if(a>5)
3.{
4.    System.out.println(a);
5.}
6.//其他代码

转换到的跳转指令可能是:

1.int a= 10;
2.条件跳转:如果a>5,跳转到第4行
3.无条件跳转:跳转到第7行
4.{
5.    System.out.println(a);
6.}
7.//其他代码

switch的实现也是同上述代码原理相同,转换成跳转指令。但是switch的转换和具体系统实现有关。如果分支比较少,可能会转换为跳转指令。如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方式,叫跳转表。跳转表是一个映射表,存储了可能的值以及要跳转到的地址,下表所示:

那么问题来了,跳转表为什么会更为高效呢?

因为其中的值必须为整数,且按大小顺序排序(源程序中case值排序并不要求,编译器会自动排序)。按大小排序的整数可以使用高效的二分查找,即先与中间的值比,如果小于中间的值,则在开始和中间值之间找,否则在中间值和末尾值之间找,每找一次缩小一半查找范围。如果值是连续的,则跳转表还会进行特殊优化,优化为一个数组,连找都不用找了,值就是数组的下标索引,直接根据值就可以找到跳转的地址。即使值不是连续的,但数字比较密集,差的不多,编译器也可能会优化为一个数组型的跳转表,没有的值指向default分支。

之前说switch值的类型可以用byte、short、int、char、枚举、和String。为甚是这几种呢?其他的不能行吗?

实际上switch需要的是整数,或者说与整型相兼容的。其中byte/short/int本身就是整数,人char本质上也是整数(比如 'a' 是97,我们是知道的哟)。而枚举类型也有对应的整数,String用于switch也会转换为整数(通过hashCode转换)。

为什么不能用Long类型呢?它也是整数啊

为什么呢?跳转表值得存储空间一般为32位,容不下long。!!!∑(゚Д゚ノ)ノ


接下来讨论switch中使用字符串需要注意的问题

我们知道case子句的值不能重复。而对于字符串来说,这种重复值的检查还有一个特殊之处。那就是Java代码中的字符串可以包含Unicode转义字符。重复值的检查是在Java编译器对Java源代码进行相关的词法转换之后才进行的。这个词法转换过程中包括了对Unicode转义字符的处理。也就是说,有些case子句的值虽然在源代码中看起来是不同的,但是经词法转换后是一样的,这就会造成编译错误。如下面的代码:

public class Persion {
    public String getMsg(String name, String gender) {
        String msg = "";
        switch (gender) {
              case "男" :
                  break;
              case "\u7537":
                  break;
        }
        return msg;
    }
}

上面代码中,类Persion是无法通过编译的。因为“男”与“\u7537”经过此法转换之后变成一样的了。

switch中使用String是怎么实现的呢?

switch中使用String是从java7开始支持的新特性,是在编译器这个层面上实现的。在编译的过程中,编译器会根据源代码的含义来进行转换,将字符串类型转换成与整数类型兼容的格式。不同的Java编译器可能采用不同的方式来完成这个转换,并采用不同的优化策略。举例来说,如果switch语句中只包含一个case子句,那么可以简单地将其转换成一个if语句。如果switch语句中包含一个case子句和一个default子句,那么可以将其转换成if-else语句。而对于最复杂的情况,即switch语句中包含多个case子句的情况,也可以转换成Java7之前的switch语句,只不过使用字符串的哈希值作为switch语句的表达式的值。

为了探究编译器是怎么样转换的,我们通过JAD工具来将编译好的class文件反编译成java源文件

如下面的源代码:

package testSwitch;

public class TestSwitch {
    public static void main(String[] args) {
        printYourName("小白");
    }

    public static void printYourName(String s){
        switch (s){
            case "小白":
                System.out.println("你的名字是:小白");break;
            case "小灰":
                System.out.println("你的名字是:小灰");break;

        }
    }
}

编译后形成 TestSwitch.class文件,通过jad工具反编译后形成的TestSwitch.jad文件内容如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   TestSwitch.java

package testSwitch;

import java.io.PrintStream;

public class TestSwitch
{

    public TestSwitch()
    {
    }

    public static void main(String args[])
    {
        printYourName("\u704F\u5FD5\u6AE7");
    }

    public static void printYourName(String s)
    {
        String s1 = s;
        byte byte0 = -1;
        switch(s1.hashCode())
        {
        case 28417601: 
            if(s1.equals("\u704F\u5FD5\u6AE7"))
                byte0 = 0;
            break;

        case 28410464: 
            if(s1.equals("\u704F\u5FD5\u4F06"))
                byte0 = 1;
            break;
        }
        switch(byte0)
        {
        case 0: // '\0'
            System.out.println("\u6D63\u72B5\u6B91\u935A\u5D85\u74E7\u93C4\uE224\u7D30\u704F\u5FD5\u6AE7");
            break;

        case 1: // '\001'
            System.out.println("\u6D63\u72B5\u6B91\u935A\u5D85\u74E7\u93C4\uE224\u7D30\u704F\u5FD5\u4F06");
            break;
        }
    }
}

通过反编译发现,case子句中的值被转换成为字符串的hash值,而后面的语句中仍然使用的是String的equals()方法来比较的。

为什么使用equals()方法来比较,而不是用hash值来比较呢?

这是因为哈希函数在影射的时候可能存在冲突,多个字符串的哈希值可能是一样的。进行字符串的比较是为了保证转换之后的代码逻辑与之前完全一样。

既然对字符串的哈希值可能一致,那么case子句的哈希值会不会重复呢?case子句值重复可是编译不通过的呢!

答案是肯定会重复的,如下面的代码s1与s2的值并不相同但是他们输出的哈希值都是【165374702】:

public class TestHash {
    public static void main(String[] args) {
        String s1 = "ABCDEa123abc";
        String s2 = "ABCDFB123abc";
        System.out.println(s1.hashCode());
        System.out.println(s2.hashCode());   
    }
}

那么,下面这段代码,两个case所表示的哈希值相同,也就是case值相同,但是为什么编译不报错呢?

public class TestHash {
    public static void main(String[] args) {
        String s1 = "ABCDEa123abc";
        String s2 = "ABCDFB123abc";
        testStringSwitch(s1);
    }
    public static void testStringSwitch(String s){
        switch (s){
            case "ABCDEa123abc": System.out.println(1); break;
            case "ABCDFB123abc": System.out.println(2); break;
        }
    }
}

为了解决这个问题我们再次使用jad工具对TestHash类编译后形成的TestHash.class文件进行反编译,反编译后的结果内容如下:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   TestHash.java

package testSwitch;

import java.io.PrintStream;

public class TestHash
{

    public TestHash()
    {
    }

    public static void main(String args[])
    {
        String s = "ABCDEa123abc";
        String s1 = "ABCDFB123abc";
        System.out.println(s.hashCode());
        System.out.println(s1.hashCode());
        testStringSwitch(s);
    }

    public static void testStringSwitch(String s)
    {
        String s1 = s;
        byte byte0 = -1;
        switch(s1.hashCode())
        {
        case 165374702: 
            if(s1.equals("ABCDFB123abc"))
                byte0 = 1;
            else
            if(s1.equals("ABCDEa123abc"))
                byte0 = 0;
            break;
        }
        switch(byte0)
        {
        case 0: // '\0'
            System.out.println(1);
            break;

        case 1: // '\001'
            System.out.println(2);
            break;
        }
    }
}

通过观察,我们可以清楚的发现:当case子句的hash值形同的时候,编译阶段只会转换形成一条case子句,也就是说两个case子句合并成了一条!!两个子句的后续语句转换成了if/else if语句。

猜你喜欢

转载自www.cnblogs.com/nm666/p/10686816.html