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語句。