用java比较abc大小的程序_Java中 "abc" + '/'和"abc" + "/"的区别

好吧,写这篇博客又是因为一个有趣的帖子。原问题是“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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值