好吧,写这篇博客又是因为一个有趣的帖子。原问题是“String str = “abc” + ‘/’;和”abc” + “/”;的区别”,感觉这个问题相当有意思,我们天天使用String类,但是有谁考虑到这么细致的问题了。这里给出的是我的个人见解,限于本人水平,有什么错误还请大家见谅
原问题
把斜杠/当作字符或字符串有什么区别呢?
一个是当作基本数据类型char,一个是对象String。具体有什么区别呢?
当作字符效率会更高吗?
String str = "abc" + '/';
和
String str = "abc" + "/";
看到这个问题以及下面几条回复,我首先想到的是,查看这些语句的调用链:
'/'到底是通过valueOf()方法变成字符串的,还是通过构造方法变成字符串的。"/"呢,又是通过哪个途径呢?于是有了第一次尝试。
修改String类的代码,记录调用链。失败
由于以前的坏习惯,不喜欢调试,习惯直接在代码中加入打印语句,打印一些变量或者标记程序执行流程。于是,要查看调用链?好,修改String类代码,在所有参数为char或者char[]的valueOf()方法和构造方法里面加一句打印语句。结果发现直接把JDK弄挂了,连编译Hello World都不行了(原因至今不明,可能是因为System.out为null吧)。然后,改成写文件,写一个方法,调用堆栈层数大于6(防止死递归)或者线程名不是main(排除无用信息)的直接返回,满足条件的把信息写到一个文件中。仍旧失败。后来发现,获取调用堆栈信息也得用String类,于是,死递归了。。。然后JDK又挂了。貌似编译程序时,也得用到一些Java写的程序,而作为最常用的String类的构造方法出现了死递归,结果就栈溢出了,,,
好吧,第一次花了我一天时间,结果还是失败的!?然后突然就想到javap了(注:关于javap可以看看这个博客:《每个Java开发者都应该知道的5个JDK工具》),于是开始第二次尝试。。。
第一次使用javap,失败
有了思路,就开始动手。于是写了这个测试程序:
public class Test{
public static void main(String[] args){
String str1 = "abc" + '/';
String str2 = "abc" + "/";
System.out.println(str1 == str2);
}
}
javac Test.java
javap -l -c -v Test.class > Test.s
然后看Test.s(注:javap生成文件中的指令的含义可以参考这几篇博客:《Java栈和局部变量操作(一)》,《Java栈和局部变量操作(二)》,《java指令集》):
...
Constant pool:
#1 = Methodref #6.#19 // java/lang/Object."":()V
#2 = String #20 // abc/
#3 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
...
0: ldc #2 // String abc/
2: astore_1
3: ldc #2 // String abc/
5: astore_2
注:下面使用的“行号”是指指令前的数字,如0-2是指第7-8行
0-2:从常量池中取出"abc/"的引用赋给第一个变量,也就是str1
3-5:从常量池冲取出"abc/"的引用赋给第二个变量,也就是str2
好吧,被优化掉了,,,又找不到关闭优化的选项,只找到一个开启优化选项-O,还是默认关闭的!既然常量会被优化掉,那么变量呢?于是开始了第三次尝试
再次使用javap
这次修改代码:
public class Test{
public static void main(String[] args) {
String str = "abc";
char ch1 = '/';
String ch2 = "/";
System.out.println((str+ch1) == (str+ch2));
}
}
既然常量会被优化掉,那么就使用变量。这次使用javap反编译产生的文件大多了:
...
Constant pool:
#1 = Methodref #12.#25 // java/lang/Object."":()V
#2 = String #26 // abc
#3 = String #27 // /
...
Code:
stack=4, locals=4, args_size=1
0: ldc #2 // String abc
2: astore_1
3: bipush 47
5: istore_2
6: ldc #3 // String /
8: astore_3
9: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
12: new #5 // class java/lang/StringBuilder
15: dup
16: invokespecial #6 // Method java/lang/StringBuilder."":()V
19: aload_1
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: iload_2
24: invokevirtual #8 // Method java/lang/StringBuilder.append:(C)Ljava/lang/StringBuilder;
27: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: new #5 // class java/lang/StringBuilder
33: dup
34: invokespecial #6 // Method java/lang/StringBuilder."":()V
37: aload_1
38: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: aload_3
42: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
45: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
48: if_acmpne 55
51: iconst_1
52: goto 56
55: iconst_0
56: invokevirtual #10 // Method java/io/PrintStream.println:(Z)V
59: return
从这里已经可以看出这段代码的执行过程了:
0-2:从常量池中取出”abc”的引用压栈,再弹栈赋给第一个变量(不知道为什么非得压栈再弹栈,为什么不直接赋值?)
3-5:将47从byte型变为int压栈,再弹栈赋给第二个变量。
6-8:从常量池中取出”/”的引用压栈,再弹栈赋给第三个变量
…(以下会省略压栈、弹栈等操作,因为两段完全一样。有兴趣的读者可以自己分析一下)
12-27:接着创建一个StringBuilder类实例,使用第一个变量(“abc”)作为参数调用append方法,使用第二个变量(‘/’)为参数调用append方法,调用toString方法获得字符串
30-45:然后再创建一个StringBuilder类实例,使用第一个变量(“abc”)作为参数调用append方法,使用第三个变量(“/”)为参数调用append方法,调用toString方法获得字符串
比较,输出…
那么,比较"abc"+'/'和"abc"+"/"的区别的问题就变为比较StringBuilder类的append(char ch)方法和append(String str)方法了。
StringBuilder类的append(char)方法和append(String)方法
这里,我们直接查看源码就好了(我的是jdk1.8.0_45附带的源码)。
注意,查看的是抽象类AbstractStringBuilder的源码,虽然文档上没有显示,但是StringBuilder确实继承自抽象类AbstractStringBuilder。而且append方法是由AbstractStringBuilder实现的。
AbstractStringBuilder.append(char):
public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1); // 确保数组能够容纳count+1个字符
value[count++] = c;
return this;
}
AbstractStringBuilder.append(String):
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count); // 拷贝字符串中的字符数组到本对象的字符数组中
count += len;
return this;
}
剩下的就不再贴出来了。String.getChars(int, int, char[], int)最终依赖于public static native void arraycopy(Object, int, Object, int, int)。也就是说极有可能是C语言甚至是汇编等语言写的,在拷贝大型数组时效率应该会比一般java写的程序好一些。
那么,现在说说我的理解:
从直接内存来说,由于String中包含char数组,而数组应该是有长度字段的,同时String类还有一个int hash属性,所以字符串会多占用一些内存。但是如果字符串非常长,那么这两个字段的内存开销差不多就可以忽略了;而如果像"/"这种情况,字符串比较(非常)短,那么就会有许多个共享引用来分担这些内存开销,那么多余的内存开销还是可以忽略的。
从调用堆栈上,由于这里String只比char多了一两层函数调用,所以如果不考虑函数调用开销(包括时间和空间),应该差不多;考虑函数调用开销,应该 "abc" + '/'更好一些;但是当需要连接若干个字符时(感觉这种情况应该更常见吧?),由于使用char需要循环好多次才能完成连接,调用的函数次数只会比使用String多吧?同时拷贝也不会比String直接拷贝一个数组更快。所以这个时候就变成了"abc" + "/"更好了。
现在感觉这个问题像是在问:读写文件时使用系统调用效率高,还是使用标准函数库中的IO库效率高。个人感觉,虽然标准IO库最后还得调用系统调用,而且这之间会产生一些临时变量,以及更深层次的调用堆栈,但是由于IO库的缓冲,反倒是IO库的吞吐量更大一些。同样,虽然String类会多几个字段,有更深层次的函数堆栈,但是由于一些缓存以及更直接的拷贝,效率应该会更好一些。
新的问题
好吧,其实这里又产生一个新的问题:老师告诉我们,当有大量字符串连接操作时,StringBuffer比String更好,更省内存。StringBuilder的文档告诉我们:
如果可能,建议优先采用该类(StringBuilder),因为在大多数实现中,它比 StringBuffer 要快。
而我们看到字符串连接的执行过程实际上创建了一个临时的StringBuilder对象,那么StringBuffer到底是不是真的比String更好,更省内存呢?大家可以参考我写的另一篇博客:《java中String和StringBuffer哪个效率高》.
结语:
这两天的经历让我真正的体会到了一个道理:坚持很重要,但有时换个方向继续坚持才能更快的到达目标!
写于2015/04/24