Java中的字符串缓冲池——String pool

来源:互联网 发布:中国移动短信群发软件 编辑:程序博客网 时间:2024/06/06 08:47

一、简介

字符串操作是计算机程序设计中最常见的行为。正因为常用,Java在对String类的设计上实现的原理时常会发生变化,在后来的jdk6、7、8中,对于优化的改进一直在进行。

对于同一段代码,在不同版本下的运行结果可能会出现不同的情况:

<span style="white-space:pre"></span>    String s = new String("1");    s.intern();    String s2 = "1";    System.out.println(s == s2);    String s3 = new String("1") + new String("1");    s3.intern();    String s4 = "11";    System.out.println(s3 == s4);        String s5 = new String(new char[]{'a' , 'b'});    s5.intern();    String s6 = "ab";    System.out.println(s5 ==s6);
对于上面的代码在jdk6下的结果是:false false false但是在jdk7中:false true true;主要是因为JVM对“入池”操作的实现原理发生了些许变化。但是在常用的字符串操作还是遵循“任何看似改变字符串的操作,都不会改变字符串本身,改变的只是字符串的副本”。

二、工作原理

java的虚拟机在内存中开辟出一块单独的区域,用来存储字符串对象,这块内存区域被称为字符串缓冲池。当代码中出现字面量形式创建字符串对象时,JVM首先会对这个字面量进行检查,如果字符串常量池中存在相同内容的字符串对象的引用,则将这个引用返回,否则新的字符串对象被创建,然后将这个引用放入字符串常量池,并返回该引用。

例如代码:String str1 = "aaa";

 创建字符串的时候先查找字符串缓冲池中有没有相同的对象,如果有相同的对象就直接返回该对象的引用,如果没有相同的对象就在字符串缓冲池中创建该对象,然后将该对象的应用返回。对于这一步而言,缓冲池中没有aaa这个字符串对象,所以首先创建一个字符串对象,然后将对象引用返回给str。

String str2 = "aaa";

对于这一句,也是想要创建一个对象引用变量str2使其指向aaa这一对象。这时,首先查找字符串缓冲池,发现aaa这个对象已经有了,这是就直接将这个对象的引用返回给str2,此时str1和str2就共用了一个对象aaa,这时会出现一个疑问,两个变量引用同一个对象,此时,str1改变了这个对象,str回不回受影响呢?不用担心,因为字符串都是常量,一旦创建就没办法修改了,除非创建一个新的对象。

所有的String对象都是不可变对象,所有的看似改变String对象的操作都只不过是创建了一个新的String对象,而原先的对象丝毫未变。如:

String str1 = "aaa";String str2 = "aaa";System.out.println(str1.toUpperCase());//AAAstr1 += "bbb";System.out.println(str1);//aaabbbSystem.out.println(str2);//aaa

输出结果为:

AAA

aaabbb

aaa

可以发现无论是转换成大写操作还是拼接操作都没有改变str2的引用值。 

继续代码:

String str3 = new String("aaa");

String str4 = new String("ccc");

上面中的代码执行时:

第一句:先判断字符串缓冲池中是否有"aaa"对象,此时已经存在,不再创建,只是将“aaa”的引用返回给str3,此句创建了一个对象。注意,str2 不是对象,只是引用.只有 new 生成的才是对象.

第二句:先判断字符串缓冲池中是否有"ccc"对象,此时不存在,创建“ccc”对象,再将“ccc”的引用返回给str4。

String a = "abc";String b = "abc";String c = new String("xyz");String d = new String("xyz");String e="ab"+"cd";

这个程序与上边的程序比较相似:

String a = “abc”;这一句由于缓冲池中没有abc这个字符串对象,所以会创建一个对象;String b = “abc”;由于缓冲池中已经有了abc这个对象,所以不会再创建新的对象;String c = new String(“xyz”);由于没有xyz这个字符串对象,所以会首先创建一个xyz的对象,然后这个字符串对象由作为String的构造方法,在内存中(不是缓冲池中)又创建了一个新的字符串对象,所以一共创建了两个对象;String d = new String(“xyz”);省略了创建一个对象的过程,所以只创建了一个对象;String e=”ab”+”cd”;由于常量的值在编译的时候就被确定了。所以这一句等价于String e=”abcd”;创建了一个对象。

在学习java时就知道两个字符串对象相等的判断要用equal而不能使用==,但是学习了字符串缓冲池以后,应该知道为什么不能用==,什么情况下==和equal是等价的,首先,必须知道的是,==比较的是两个对象的内存地址是否相等。

String a = "abc";String b = "abc";if(a==b){System.out.println("a==b");}else{System.out.println("a!=b");}

输出结果为: a==b

 此时使用==可以进行比较的原因就是a和b引用的是字符串缓冲池中的同一个对象,他们的物理地址相同,所以值是相同的。

String a = "abc";String c = new String("abc");if(a==c){System.out.println("a==c");}else{System.out.println("a!=c");}if(a.equals(c)){System.out.println("a.equals(c)");}else{System.out.println("!a.equals(c)");}

结果为:

 

a!=ca.equals(c)

String c = new String("abc");这一句话没有在字符串缓冲池中创建新的对象,但是会在内存的其他位置创建一个新的对象,所以a是指向字符串缓冲池的,c是指向内存的其他位置,两者的内存地址不同的。

String a = "abc";String c = new String("abc");c = c.intern();if(a==c){System.out.println("a==c");}else{System.out.println("a!=c");}if(a.equals(c)){System.out.println("a.equals(c)");}else{System.out.println("!a.equals(c)");}

在代码中增加了 c = c.intern();语句后,结果为:

a==ca.equals(c)

intern()方法的作用是返回在字符串缓冲池中的对象的引用,所以c指向的也是字符串缓冲池中的地址,和a是相等的。

String Monday = "Monday";          String Mon = "Mon";          String  day = "day";          System.out.println(Monday == "Mon" + "day");          System.out.println(Monday == "Mon" + day);

结果为:

true
false

第一个为true ,因为两者都是常量所以在编译阶段就已经能确定了,在第二个中,day是一个变量,所以不能提前确定他的值,所以两者不相等,从这个例子我们可以看出,只有+连接的两边都是字符串常量时,引用才会指向字符串缓冲池,否则指向内存中的其他地址。

String Monday = "Monday";          String Mon = "Mon";          final String  day = "day";          System.out.println(Monday == "Mon" + "day");          System.out.println(Monday == "Mon" + day);

加上final后day也变成了常量,所以第二句的引用也是指向的字符串缓冲池。

在面试的问题中,关于字符串创建的内容很常见:

在下面的语句中字符串常量池中含有的是?

String str = "a" + "b" + "c";

答案是只有一个:"abc",还是上面同样的原理,由于在编译的时候变量str的值就已经确定了,查看一下编译后的文件内容:

String str = "abc";

代码直接优化成了“abc”,这样可以减少在运行时的操作步骤。

在JDk7版本中对于字符串的改变

首先是在switch语句中添加了对字符串的支持。

其次是对字符串常量池使用的内存位置进行了移动,以适应更大的数据量。

关于intern操作

有两篇讲解的很好的博客:

http://www.cnblogs.com/paddix/p/5326863.html#undefined

http://tech.meituan.com/in_depth_understanding_string_intern.html

博客中对于此方面的讲解的总结:

jdk6版本中字符串所有的方法偶遵循“任何看似改变字符串的操作,都不会改变字符串本身,改变的只是字符串的副本”,即使是intern操作也不例外;从JDK 1.7后,HotSpot 将常量池从永久代移到了元空间,正因为如此,JDK 1.7 后的intern方法在实现上发生了比较大的改变,JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用,所以在开始的程序中出现了结果为true的情况,就是因为两个对象指向了同一个引用。此时会不会有这样的疑问:这样会不会出现引用传递的情形,两个对象中的其中一个对引用进行了修改,进而影响了另外一个对象。这样的情形是不会出现的,字符串的操作还是遵循不变的原则的,即使一个引用改变了值也只是改变了自身的引用,并不会改变另外一个对象。对象的引用占用的空间远比实际值占用的空间小,对于含有大量字符串操作的程序,这样的改进能够避免占用大量的内存影响程序运行速度。

0 0