zoukankan      html  css  js  c++  java
  • 深入理解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("u704Fu5FD5u6AE7");
        }
    
        public static void printYourName(String s)
        {
            String s1 = s;
            byte byte0 = -1;
            switch(s1.hashCode())
            {
            case 28417601: 
                if(s1.equals("u704Fu5FD5u6AE7"))
                    byte0 = 0;
                break;
    
            case 28410464: 
                if(s1.equals("u704Fu5FD5u4F06"))
                    byte0 = 1;
                break;
            }
            switch(byte0)
            {
            case 0: // ''
                System.out.println("u6D63u72B5u6B91u935Au5D85u74E7u93C4uE224u7D30u704Fu5FD5u6AE7");
                break;
    
            case 1: // '01'
                System.out.println("u6D63u72B5u6B91u935Au5D85u74E7u93C4uE224u7D30u704Fu5FD5u4F06");
                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: // ''
                System.out.println(1);
                break;
    
            case 1: // '01'
                System.out.println(2);
                break;
            }
        }
    }
    
    

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

  • 相关阅读:
    [Scala] akka actor编程(一)
    随便说说
    [Java] Java执行Shell命令
    [Scala] Scala基础知识
    [Linux] 账户管理命令(二)
    [Linux] 账户管理命令(一)
    [Kerberos] Java client访问kerberos-secured cluster
    沟通与影响技术培训
    Python
    ML 基础知识
  • 原文地址:https://www.cnblogs.com/nm666/p/10686816.html
Copyright © 2011-2022 走看看