java字符串之理论重点
1、简单概述
一直以来,对String这个字符串类都是心存疑惑的,也不知道如何来进行解决。最近花点时间看了下别人的博客来研究一下,觉得挺有意思的,所以将此来记录下来。
String将会牵扯到堆和常量池,字符串常量池在JDK8中是存在于堆中的,是属于堆的一部分。
引入:
String str = "计算机";
"computer"也是一个字符串对象,只不过上面创建的这个会存在于字符串常量池中,返回来的是在字符串在常量池中的地址。
String str = "计算机";
String str2 = "计算机";
这段代码表示的是都在字符串常量池中来创建"计算机"对象,但是由于第一行中已经在字符串常量池中创建好了"计算机"对象,所以第二行代码将不会再次进行创建,而是直接指向字符串常量池的中"计算机"的地址。因为常量池是用来节省内存资源开销的,将一些经常使用的常量放在里面来方便来使用。
测试这段代码:
String str = "计算机";
String str2 = "计算机";
System.out.println(str==str2); // true
因为二者指向的是在常量池中得同一个地址,所以返回为true
上面两种创建字符串的方式也是官方推荐的,但是和使用new关键字创建出来的字符串有很大的不同的使用
String str = new String("计算机");
首先看到了因为出现了字符串常量"计算机",那么首先做的事情就是检查字符串常量池中有没有"计算机",如果没有,那么添加进去;如果有,那么就不添加;
然后第二步,因为是new,所以会在堆中创建出来"计算机"对象
再看一段代码:
String str = new String("计算机");
String str2 = new String("计算机");
执行第一行代码的时候,先去字符串常量池中检查是否存在"计算机"对象,如果存在,那么不创建;如果不存在,那么就在字符串常量池中创建该对象;
然后执行new的步骤,会在堆内存中来创建出"计算机"对象,str引用指向新开辟的内存空间地址;
然后执行第二行代码,先去字符串常量池中检测是否存在"计算机"对象,发现已经存在了,那么就不再进行创建;
然后执行new的步骤,会在堆内存中创建出来"计算机"对象,str2引用指向新开辟的内存空间地址
最终的图如下所示:
所以当在检测的时候
String str = new String("计算机");
String str2 = new String("计算机");
System.out.println(str==str2); // false
2、intern()方法
intern()方法也是面试中的经常问到的地方,所以在这个地方也会来做一个说明。
jdk1.8中是这样描述intern()方法的:当intern()方法被调用的时候,如果字符串常量池中已经存在这个字符串对象了,就返回常量池中该字符串对象的地址;如果字符串常量池中不存在,就在常量池中创建一个指向该对象堆中实例的引用,并返回这个引用地址。
这里想要表达的意思就是说:
if(字符串常量池中存在){
return 常量池中地址;
}
if(常量池中不存在){
return 创建指向堆中字符串的地址的引用;
}
这里也用图来进行说明下:
String str1 = new String("计算机")+new String("软件");
String str2 = str1.intern();
使用上述的代码来验证一下:
String str1 = new String("计算机")+new String("软件");
String str2 = str1.intern();
System.out.println(str1 == str2); // true
但是在这里,我需要说明一下上面的第一行代码:
String str1 = new String("计算机")+new String("软件");
这里在常量池中存在的字符串只有"计算机"和"软件",不会存在"计算机软件",从上面的图中可以看到。
验证一下:
如果说:String str1 = new String("计算机")+new String("软件");这段代码执行完成之后,存在了"计算机软件",那么将直接会返回在字符串常量池中的地址,这个时候如果和堆中得地址来进行比较,那么返回的应该是false,但是这里返回的是true,可以验证。
而执行
String str2 = str1.intern();
这段代码后,就会在字符串常量池中来创建"计算机软件"对象,因为字符串常量池中是没有的,但是堆中有,所以创建了一个指向堆中的常量的对象,这个对象也是"计算机软件",但是这个对象实际指向的却是堆中的地址:
用一个例子说明下:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
第一步中:首先检查字符串常量池中是否存在着"1"对象,如果不存在,那么在字符串常量池中来进行创建;如果存在了,那么不需要来进行创建;最终会在堆中创建两个"1"对象,因为有两个new。
然后在堆中会存在"11"对象,其实这个和上面的一样,两个new出来的对象进行拼接的时候,到底是在堆中产生了新的对象还是在字符串常量池中,我觉得可以来进行证明:
String s3 = new String("1") + new String("1");
String s4 = "11";
System.out.println(s3 == s4); // false
证明了第一行中产生的对象是在堆中的,直接new的话就会在常量池中。
那么再接着说上面的问题:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
因为第一行中产生的"11"是在堆内存中的,如果第二行执行了,如果常量池中有,那么就直接返回其地址了;如果没有,那么就直接返回在堆内存中的地址;很明显,字符串常量池中是没有"11"的,只存在于堆中,那么执行完成之后,会在字符串常量池创建指向堆内存的引用,然后将这个引用保存在字符串常量池中。
然后走到第三行的时候,会去检查"11"是否存在于常量池中,检查到了有一个引用存在于常量池中,但是实际指向的却是堆中的,所以s4也指向了堆中的地址。这里底层应该是做了某种优化方式。
看看最终的结果:
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3==s4); // true
这里还应该注意到一个问题,就是顺序的问题:
String s4 = "11";
String s3 = new String("1") + new String("1");
s3.intern();
System.out.println(s3==s4); // false
第一行:首先直接在字符串常量池中创建一个对象"11",
第二行:首先在堆中创建"1",然后在堆中创建"11";
第三行:因为字符串常量池中已经存在了"11"对象,所以将常量池中得字符串对象"11"地址进行返回;
第四行:堆中地址和常量池中地址进行比较,返回FALSE
面试题:
String s1 = "1";
String s2 = new String("1");
这段代码中创建了几个对象?
第一步:检查字符串常量池中是否存在"1",如果不存在,那么创建"1"对象;如果存在,就不会对象"1"对象;
第二步:检查字符串常量池中是否存在"1",存在,则不会在字符串常量池中来创建,直接new一个"1"在字符串常量池中;
所以这段代码可能创建了1个对象,也可能创建了2个对象。
因为new肯定是会在堆中来创建对象的,至于是否需要在字符串常量池中创建,取决于字符串常量池中是否存在了"1"对象;
3、总结
字符串对象一定位于堆中,到底是字符串常量池还是堆中得除了常量池的其他位置,需要按照具体情况来定。
注意的两种情况:
String str1 = "aaa";
String str2 = new String("aaa");
在结合intern()方法的使用分析即可。
有一点需要来补充说明下,字符串常量池中也缓存了一些默认的字符串。比如"java",但是到底缓存了多少个,那我们就不得而知了。可以验证下:
String s1 = new String("ja")+new String("va");
String s2 = s1.intern();
System.out.println(s1==s2); // false
但是要是改变了其中的一点,那么结果将会变化:
String s1 = new String("ja")+new String("va1");
String s2 = s1.intern();
System.out.println(s1==s2); // true
因为java中得字符串常量池中缓存了"java"这个字符串,只不过我们无法看到而已。