Java编程优化之旅(二) String类型知多少
___________________________________________________________________________
String 类大概是Java编程中用到最多的类。一段文本,一个URL,一个Email,甚至一串电话号码都是字符串。然而正是由于如此常用,所以关于String类中的一些低效率的使用方法更应该引起大家的重视。String类型博大精深,优化的方式有很多,本文也只是惊鸿一瞥,简单介绍几个小Tip。以后若想到了其他技巧,会继续撰文补充。
本文中参照的Jdk 源码是1.7。虽然String类已经稳定,但不同版本的 Jdk 中 String 类的定义还是有变化的。比如我看过的《Java性能优化》一书,其中讲到String 的某些源码已经和如今的源码不同了。
___________________________________________________________________________
String空串的判断
通过看String的源码,可以发现String类中实际在存储字符串的是一个字符数组,源码中有这么一句:
private final char value[];
空串一般有两种含义,一种是“”,另一种是null。null表示不指向任何东西,如果进行比较等操作会出现“空指针异常”。而“”是一个没有字符的字符串,在内存中确有所指。通常我我们比较一个字符串str是否为空串的常用写法为:
if(str != null && !str.equals("")) { }
也就是说要把两种情况都要考虑进去。【请注意】:不能写成 !str.equals("")&&str!=null 因为根据语句执行的顺序,如果str真的是null的话,那么执行str的equals方法必然会报 “空指针异常”的错误。所以要先判断是否为null 不为null ,才能进去比较。
然而,str.equals("")是有进行优化的潜力的。因为equas方法,着实是个比较费力的操作。请看源码:
public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
注意,即使是空字符串 "",也是一个Object(对象)。所以以上语句都会执行。所以无形之中降低了效率。更为高效的比较空串的方法是:
str.length() != 0
String的length() 方法直接返回 字符数组value的长度:
//源码 public int length() { return value.length; }
直接来判断 String 对象持有的字符数组的大小是否为0,是不是更高效呢?此外,在Jdk 1.6 开始出现了一个新的方法 isEmpty()
public boolean isEmpty() { return value.length == 0; }
显然是一种语法糖了。所以最后,我们判断一个字符串是否为空串的高效表达为:
if(str != null && str.length() != 0) { } //或(jdk >=1.6) if(str != null && !str.isEmpty()) { }
字符串连接
少吃语法糖“+”
String的contact()方法
使用StringBuffer来连接
StringBuffer sb = new StringBuffer() ; for(int i=0;i<10000;i++) sb.append("x"); String s= sb.toString();在我电脑里的执行时间几乎每次都在10ms以下,多数情况下只有1,2 ms的样子。
String s1 = ""; System.out.println(s1); for(int i=0;i<10000;i++) s1 = s1.concat("x");在我的电脑中执行时间大概100多ms。
String s = ""; for(int i=0;i<10000;i++) s = s + "x";在我电脑里的执行时间是超过200ms接近300ms。可见,这最甜的语法糖很可能是你的性能毒药。
-------------------------------------------------------------------------------
charAt()方法的小讨论
charAt() VS toCharArray()
作者的观点是这样的:在比较一个字符串的每个字符(或者要比较大量字符)的时候。可以分两步走,①可以先使用toCharArray方法获得这个String对象的字符数组。②对获得的字符数组进行字符的比较。诚然,②比较Char数组中字符的开销要小于使用字符串对象一次次调用charAt方法来比较字符的开销,然而,作者可能忽略了一点,那就是在开始的时候①获得字符数组的
toCharArray方法的开销也是不容小觑的。为此,我做了几次测试:
int size = 10000; StringBuffer sb = new StringBuffer(); for (int i = 0; i < size; i++) sb.append('x'); String s = sb.toString(); long time = System.currentTimeMillis(); for (int i = 0; i < s.length(); i++) if (s.charAt(i) == 'x') { } System.out.println(System.currentTimeMillis() - time); time = System.currentTimeMillis(); char ss[] = s.toCharArray(); for (int i = 0; i < ss.length; i++) if (ss[i] == 'x') { } System.out.println(System.currentTimeMillis() - time);运行10次的结果:
目前来看,上面作者的结论貌似是对的。然而接下来。我改变上面代码中size的大小,也就是String对象所包含的的字符的个数。
int size = 10000000;//比上一次代码多了三个0,也就是扩大1000呗然后运行10次的结果:
可以看出charAt方法以绝对优势胜出了。这是字符串的长度扩大1000倍后的结果。实际上在我的电脑上,在第一次size的基础上扩大100倍后,charAt方法的优势也并不明显,然而1000倍后就凸显出来了。大概的性能曲线可能是这样:在字符串的长度不是很大的情况下,使用toCharArray方法是高效的,但若字符串长度很大的时候,使用toCharArray的性能就会降低,因为花了高额的代价来获取字符数组(也就是前面分析中的第一步)。
—————————————————题外话,性能测试的小Tip————————————————————
为此,我和原作者往来了两封Emil。他也说是那篇论文写得时间比较早了(1999)。所以Java的编译器和运行环境有了很大改进。使得chatAt方法的性能得到了很大的提升。
由此可见,Java性能低下的观念已经有些过时,除了硬件的进步带来的性能提高以外,Java语言本身(源码,编译器,运行环境等)也在不断进步着。
此外,作者还给我提了个测试性能时,定时的小建议:避免使用空循环。比如我上面的语句:
long time = System.currentTimeMillis(); for (int i = 0; i < s.length(); i++) if (s.charAt(i) == 'x') { } System.out.println(System.currentTimeMillis() - time);我的 if 语句体是空的。他建议的写法是:
long sum1 = 0; long time = System.currentTimeMillis(); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) >= 'a' && s.charAt(i) <= 'z') sum1 += s.charAt(i); } System.out.println(System.currentTimeMillis() - time);我想其原因大概是因为实际我们写代码的时候不会出现我上面那样的空语句,所以结果可能不够客观。因为编译器也会做优化,可能会认为是无用的语句所以根本不执行那个if括号里的语句。虽然我上面测试两个方法的时候都使用的空循环体,貌似对于性能的定性比较没有影响,但实际我们不清楚编译器到底会不会做出不同的优化来,所以我们以后尽量避免使用空语句测试。
————————————————————————————————————————
与startWith()的比较
int len = orgStr.length(); if (orgStr.charAt(0) == 'a' && orgStr.charAt(1) == 'b' && orgStr.charAt(2) == 'c') ; if (orgStr.charAt(len - 1) == 'a' && orgStr.charAt(len - 2) == 'b' && orgStr.charAt(len - 3) == 'c') ; //等价的startsWith()和endsWith()实现 orgStr.startsWith("abc"); orgStr.endsWith("abc");
- 要比较的前缀(或后缀)字符串要是在编码期已知的,也就是不能是动态字符串,比如用户输入的
- 要比较的前缀(或后缀)字符串包含的字符个数不能太多。否则 if 可不好看。
String str = "abcdefghigk";//目标串 String pre = "abcdefghig";//前缀串 long time = System.currentTimeMillis(); for (int j = 0; j < 1000000; j++)//增加循环次数,用于放大差异 if (!str.startsWith("abc")) ;//可以相应的添加不匹配的提示语句 System.out.println(System.currentTimeMillis() - time); time = System.currentTimeMillis(); int len = pre.length(); int len2 = str.length(); if (len2 < len)//目标串字符个数小于前缀串 ;//可以相应的添加不匹配的提示语句 else { for (int j = 0; j < 1000000; j++)//增加循环次数,用于放大差异 for (int i = 0; i < len; i++) if (str.charAt(i) != pre.charAt(i)) { ;//可以相应的添加不匹配的提示语句 break; } } System.out.println(System.currentTimeMillis() - time);10次运行的结果: