Java中字符串switch的实现细节

jopen 9年前

译文出处: Java译站   原文出处:javarevisited

自从Java允许在switch及case语句中使用字符串以来,许多开发人员都使用了这一特性,不过如果使用整型或者枚举的话会更好。这是JDK7中最受欢迎的特性之一,同样的还有自动资源管理以及多异常捕获。尽管我个人不太喜欢这个特性,因为使用枚举的方式其实更好,但我并不是特别反对使用它。一个原因当然是它很方便,如果程序中已经用到了字符串,这样做的确很顺手,不过我建议在生产环境的代码中使用新特性之前最好了解下它是如何工作的。我第一次听说这个特性的时候,我认为这肯定是通过equals()和hashCode()方法来实现的,我更关心的是Java 7中的字符串的switch是如何实现的。我对这个感兴趣还有一个原因,是我想在面试中问一下这个问题,如果面试中有类似这样的一个问题的话会非常有趣。验证它其实非常简单,你只需用字符串写一段switch的代码,然后反编译一下,看看编译器是如何翻译它们的就可以了。那么还等什么,赶快来看下switch中的字符串是如何工作的吧?

原始代码:

这是一个简单的测试程序,它有一个main方法,里面有一个switch块在操作String变量。程序中用到的字符串参数是运行时传递进来的,你可以从main方法的字符串数组中获取到。有三种模式来启动这个程序,主动模式,被动模式,以及安全模式。对于这些已知的确定值,其实用枚举来实现要更好,但如果你已经决定使用字符串了,你得确保你写的是大写的,不要写成小成或者骆驼式的,这样会出现大小写敏感的问题。你还可以看下这篇教程 ,了解下如何在Java 7的switch表达式中正确地使用字符串。

/** * Java Program to demonstrate how string in switch functionality is implemented in * Java SE 7 release. */   public class StringInSwitchCase {         public static void main(String[] args) {               String mode = args[0];               switch (mode) {                     case "ACTIVE":                           System.out.println("Application is running on Active mode");                           break;                     case "PASSIVE":                          System.out.println("Application is running on Passive mode");                            break;                     case "SAFE":                             System.out.println("Application is running on Safe mode");             }         }   }

编译运行这段代码需要安装JDK7才行。随便哪个版本的JDK7都可以。

反编译后的代码:

下面是上述代码使用jdk1.7.040编译后再反编译的结果。如果你是Java新手,想知道如何反编译Java类来实现逆向工程,看下<a href=”http://javarevisited.blogspot.sg/2013/01/how-to-decompile-class-file-in-java-Eclipse-javap-example.html” target=”blank”>这篇文章。JDK的每个版本都会加入越来越多的语法糖,因此对于各个水平的Java开发人员来说,知道 如何反编译Java类是想当重要的。你写的代码和实现执行的差距会越来越大。了解Java类的文件格式以及字节码指令会对你很有帮助 。Java 8最近发布了一个新的特性,叫做lambda表达式,它通过编译器来实现了内部匿名类,你可以反编译下你的类文件来看下编译器都加了些什么。

/** * Reverse Engineered code to show how String in Switch works in Java. */   import java.io.PrintStream;     public class StringInSwitchCase{         public StringInSwitchCase() { }           public static void main(string args[]) {                String mode = args[0];               String s; switch ((s = mode).hashCode()) {                     default: break;                     case -74056953:                           if (s.equals("PASSIVE")) {                                       System.out.println("Application is running on Passive mode");                            }                           break;                     case 2537357:                           if (s.equals("SAFE")) {                                 System.out.println("Application is running on Safe mode");                            }                           break;                     case 1925346054:                           if (s.equals("ACTIVE")) {                                 System.out.println("Application is running on Active mode");                            }                           break;                  }             }   }

看到这个代码,你知道原来字符串的switch是通过equals和hashCode()方法来实现的。记住,switch中只能使用整型,比如byte。short,char以及int。还好hashCode()方法返回的是int,而不是long。通过这个很容易记住hashCode返回的是int这个事实,事实上我自己都会经常忘了或者弄混。仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。因为Java编译器只增加了一个equals方法,如果你比较的是字符串字面量的话会非常快,比如”abc” ==”abc”。如果你把hashCode()方法的调用也考虑进来了,那么还会再多一次的调用开销,因为字符串一旦创建了,它就会把哈希值缓存起来,这个可以看下我自己比较喜欢的一篇文章 为什么Java中的字符串是不可变的。因此如果这个siwtch语句是用在一个循环里的,比如逐项处理某个值,或者游戏引擎循环地渲染屏幕,这里hashCode()方法的调用开销其实不会很大。不管怎样,我仍然认为使用字符串的switch来代表几个固定的值不是一个最佳实践,Java里的枚举的存在是有它的原因的,每个Java开发人员都应该使用它。

这就是Java 7如何实现的字符串switch。正如我所料,它使用了hashCode()来进行switch,然后通过equals方法进行验证。这其实只是一个语法糖,而不是什么内建的本地功能。选择权在你,我个人来说不是很喜欢在switch语句中使用字符串,因为它使得代码更脆弱,容易出现大小写敏感的问题,而且编译器又没有做输入校验 。事实上对于性能关键的代码,以前的整型常量和枚举的写法是我的最爱,在这里可读性和代码质量都更重要。事实上,99。99%的情况下,枚举都比使用字符串的switch或者整型要好,这也是它们存在于Java语言中的实际意义 。这个特性就是为了改变这种不良的编码实践而生的,我很难找到什么情况下非要针对一组输入值在switch分支中使用字符串,如果你有一个令人信服的使用字符串switch的原因,请告诉我,我或者会改变我现在的想法。

译注:更深入的话可以了解下Java在字节码层面是如何实现的,可参考这篇文章