深入理解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都的语句。
需要注意的是:
- 表达式值得数据类型只能是byte、short、int、char、枚举、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语句。