您的位置:首页 > 编程语言 > Java开发

关于Java字符串的一点探究

2014-10-22 20:46 330 查看
这几天琢磨Java内存分配,看到有个博客讲的不错,只是讲到String字符串的时候,抛了异常没有解决问题如下:

常量池中的字符串常量与堆中的String对象有什么区别呢?为什么直接定义的字符串同样可以调用String对象的各种方法呢?
带着诸多疑问,我和大家一起探讨一下堆中String对象和常量池中String常量的关系,请大家记住,仅仅是探讨,因为本人对这块也比较模糊。
第一种猜想:因为直接定义的字符串也可以调用String对象的各种方法,那么可以认为其实在常量池中创建的也是一个String实例(对象)。String s1 = new String("myString");先在编译期的时候在常量池创建了一个String实例,然后clone了一个String实例存储在堆中,引用s1指向堆中的这个实例。此时,池中的实例没有被引用。当接着执行String s1 = "myString";时,因为池中已经存在“myString”的实例对象,则s1直接指向池中的实例对象;否则,在池中先创建一个实例对象,s1再指向它。

这种猜想认为:常量池中的字符串常量实质上是一个String实例,与堆中的String实例是克隆关系。
第二种猜想也是目前网上阐述的最多的,但是思路都不清晰,有些问题解释不通。下面引用《JAVA String对象和字符串常量的关系解析》一段内容。
在解析阶段,虚拟机发现字符串常量"myString",它会在一个内部字符串常量列表中查找,如果没有找到,那么会在堆里面创建一个包含字符序列[myString]的String对象s1,然后把这个字符序列和对应的String对象作为名值对( [myString], s1 )保存到内部字符串常量列表中。如下图所示:
如果虚拟机后面又发现了一个相同的字符串常量myString,它会在这个内部字符串常量列表内找到相同的字符序列,然后返回对应的String对象的引用。维护这个内部列表的关键是任何特定的字符序列在这个列表上只出现一次。
例如,String s2 = "myString",运行时s2会从内部字符串常量列表内得到s1的返回值,所以s2和s1都指向同一个String对象。
这个猜想有一个比较明显的问题,红色字体标示的地方就是问题的所在。证明方式很简单,下面这段代码的执行结果,javaer都应该知道。
String s1 = new String("myString");
String s2 = "myString";
System.out.println(s1 == s2);  //按照上面的推测逻辑,那么打印的结果为true;而实际上真实的结果是false,因为s1指向的是堆中String对象,而s2指向的是常量池中的String常量。
<img src="https://img-blog.csdn.net/20141022205250906?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbGMzNzI5MjNhMDFtbQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

虽然这段内容不那么有说服力,但是文章提到了一个东西——字符串常量列表,它可能是解释这个问题的关键。
文中提到的三个问题,本文仅仅给出了猜想,请知道真正内幕的高手帮忙分析分析,谢谢!
以上为原作者原话,下面咱就讨论一下问题:

当第一次创建一个String对象时,如 

String str1 = new String("abc");

既然是new一个对象,肯定是在堆里分配了空间用于存放"abc",不过,除此之外虚拟机还要去常量池看看有没有"abc"这个字符串,如果没有就在常量池创建一个;如果有,就不创建了.当然str1指向堆里的内存空间。

这时,String str2 = “abc”;

str2指向常量池里的字符串地址;下面列举一段代码:

public class StringTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
String str1 = new String("a");
String str2 = "a";
String str3 = new String("a");
System.out.println(str1 == str2);
System.out.println(str1 == str3);
System.out.println(str2 == str3);
/**str1.intern()返回的字符串内存地址在常量池中*/
System.out.println(str1.intern() == str2);
System.out.println(str3.intern() == str2);

}

}
输出:

false
false
false
true
true
String对象的intern()方法返回的字符串内存地址在常量池中

在上面这个例子中 str1.intern()、str3.intern()、str2指向同一块内存,上面的问题就解决了。

关于字符串我又翻了翻《thinking in Java》这本书关于字符串那章,看到有关于“+”重载与StringBuilder的一些知识点:

关于String s = “abc”+"def";

Java底层都做了什么呢?

于是就写了如下代码:

public class Concatenation {

public static void main(String[] args) {
// TODO Auto-generated method stub
String mango = "mango";
String s = "abc"+mango+"def"+47;
System.out.println(s);
}

}
使用Jdk自带的javap来反编译上面的代码 就有了以下字节码:
进入dos,找到.class文件目录 输入:javap -c Concatenation 

public class com.lichao.string1.Concatenation {
public com.lichao.string1.Concatenation();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":
()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/GregorianCalendar
3: dup
4: sipush 1992
7: iconst_5
8: bipush 23
10: invokespecial #18 // Method java/util/GregorianCalenda
r."<init>":(III)V
13: astore_1
14: ldc #21 // String 生日是: %1$tm %1$te %1$tY
16: iconst_1
17: anewarray #3 // class java/lang/Object
20: dup
21: iconst_0
22: aload_1
23: aastore
24: invokestatic #23 // Method java/lang/String.format:(L
java/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
27: astore_2
28: getstatic #29 // Field java/lang/System.out:Ljava/
io/PrintStream;
31: aload_2
32: invokevirtual #35 // Method java/io/PrintStream.printl
n:(Ljava/lang/String;)V
35: return
}

dup 和invokevirtual语句相当于Java虚拟机上的汇编语句.即使你完全不了解汇编语言也不要紧,重点是编译器自动引入了java.lang.StringBuilder类.虽然我没有使用,但编译器却自作主张使用了它,因为它更高效.
    这个例子中,编译器创建了一个StringBuilder对象,用以构造最终的String,并为每个字符串调用了一次StringBuilder的append()方法,总计4次.最后调用toString()生成结果.并存为s(使用的命令为astore2).

既然编译器能为我们自动优化性能,接下来就看看能为我们优化到什么程度.下面的程序采用两种方式生成一个String:
方法一使用 了多个String对象;方式二在代码中使用了StringBuilder.
public class WhithStringBuider {
/**方式一*/
public static String implicit(String[] fields){
String result = "";
for(int i = 0; i < fields.length; i++)
result += fields[i];
return result;
}

/**方式二*/
public static String explicit(String[] fields){
StringBuilder result = new StringBuilder();
for(int i = 0; i < fields.length; i++)
result.append(fields[i]);
return result.toString();
}

}
javap来反编译上面的代码 就有了以下字节码:
Compiled from "WhithStringBuider.java"
public class com.lichao.string1.WhithStringBuider {
public com.lichao.string1.WhithStringBuider();
Code:
0: aload_0
1: invokespecial #8 // Method java/lang/Object."<init>":
()V
4: return
public static java.lang.String implicit(java.lang.String[]);//implicit对应的字节码
Code:
0: ldc #16 // String
2: astore_1
3: iconst_0
4: istore_2
5: goto 32
8: new #18 // class java/lang/StringBuilder
11: dup
12: aload_1
13: invokestatic #20 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
16: invokespecial #26 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
19: aload_0
20: iload_2
21: aaload
22: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
28: astore_1
29: iinc 2, 1
32: iload_2
33: aload_0
34: arraylength
35: if_icmplt 8
38: aload_1
39: areturn
public static java.lang.String explicit(java.lang.String[]);//explicit()对应的字节码
Code:
0: new #18 // class java/lang/StringBuilder
3: dup
4: invokespecial #45 // Method java/lang/StringBuilder."<
init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: goto 24
13: aload_1
14: aload_0
15: iload_2
16: aaload
17: invokevirtual #29 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: pop
21: iinc 2, 1
24: iload_2
25: aload_0
26: arraylength
27: if_icmplt 13
30: aload_1
31: invokevirtual #33 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
34: areturn
}
在implicit()中在循环内部创建StringBuilder,这意味着每循环一次,就会创建一个新的StringBuilder对象
而在explicit()方法对应的字节码中不仅循环部分更短、更简单,而且它只生成一个StringBuilder对象。显示地创建StringBuilder 还允许你预先为其指定大小.如果你已经知道最终的字符创大概有多长,预先指定StringBuilder的大小可以避免重新分配缓冲.
因此,当你为一个类编写toString()方法时如果字符串操作比较简单,哪就可以信赖编译器,它会为你合理地构造最终的字符串结果,但是,如果你要在toString()方法中使用循环,那么最好自己创建一个StringBuilder对象,用它来构造最终的结果,参考如下:

public class UsingStringBuilder {
/**使用单个 long 创建一个新的随机数生成器*/
public static Random rand = new Random(50);

public static void main(String[] args){
UsingStringBuilder us = new UsingStringBuilder();

System.out.println(us);
}

public String toString(){
StringBuilder result =  new StringBuilder("[");
for(int i = 0; i < 25; i++){
result.append(rand.nextInt(100));
result.append(",");
}
/**删除最后一个","和空格,以便添加右括号*/
result.delete(result.length() - 2, result.length());
result.append("]");

return result.toString();
}
}
输出:
[17, 88, 93, 12, 51, 61, 36, 58, 16, 8, 0, 12, 0, 55, 28, 92, 52, 7, 15, 92, 55, 23, 91, 21, 62]

最终结果是用append()语句一点点拼接起来的,如果你想走捷径,如:append(a+":"+c),那么编译器就会掉入陷阱,从而为你另外创建一个StringBuilder对象处理括号内的字符串操作.
愿读者留下评论,以便大家多多交流。
如有不足之处,望多多指点。
参考文献:《Java编程思想》第四版
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: