本篇博客是我在复习java基础时,所总结思考出的东西,面向与快速理解与面试
其中参考了我自己之前的博客与美团的技术博客。复习时直接看总结
java中String创建的不同方式以及效果
java中创建String的较为常用的两种方式
String s1 = "abc";//我是方法1
String s2 = new String("abc");//我是方法2
- 针对String s1 = “abc”;这种字面值创建方式,jvm规定,在创建时,首先查询字符串池,如果其中有值为"abc"的String对象,那么就直接返回字符串池中值为"abc"的对象,否则,会在字符串池创建值为"abc"的对象,并返回该对象。总之,**字符串池中,有则返回,无则创建返回,不管怎样,都是从字符串常量池中返回。**这里编译期便能够确定,放进常量池。
- 针对String s1 = new String(“abc”);这种new创建方式,jvm规定,首先在字符串池中查找有没有"abc"这个字符串对象,如果有,则不在池中再去创建"abc"这个对象了,直接在堆中创建一个"abc"字符串对象,然后将堆中的这个"abc"对象的地址返回;如果没有,则首先在字符串池中创建一个"abc"字符串对象,然后再在堆中创建一个"abc"字符串对象,然后将堆中这个"abc"字符串对象的地址返回。总之,字符串池中,有则不创建,无则创建,不管怎样,都是要在堆中new的String对象,都是返回堆中new的哪个对象。
好了,这里我们就奠定了一个基础,这样来分析问题的时候,我们就能够有个好的开始。
我们先来针对上面的测试一下:
String s1 = new String("abc");
问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了两个,字符串池中一个,堆中一个;如果字符串中有“abc”,则只创建了一个,就是堆中的哪个。
String s1 = "abc";
问:这句代码创建了几个对象?
答:如果字符串池中没有“abc”,则创建了一个,就是字符串池中那个;如果字符串中有“abc”,则没有创建。
intern方法
String.intern()方法,是主动将该String对象放入字符串池中,如果字符串中已经存在,则不操作,并返回字符串池中的对象,如果不存在,则放入字符串池中,并返回字符串池中的对象。
在jdk7以及之后版本,如果在使用s.intern()方法之前,如果string pool中没有s,那么当使用intern方法之后,**字符串常量池中存储的不是s的字面值,而是直接存储堆中的s的引用。**因为常量池中不需要再存储一份对象,可以直接存储堆中的引用。
下面用这个例子来测试一下
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
String s3 = new String(“1”) + new String(“1”);,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的new String(“1”)我们不去讨论它们。此时s3引用对象内容是”11”,但此时常量池中是没有 “11”对象的。
接下来s3.intern();这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 “11” 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。
最后String s4 = “11”; 这句代码中”11”是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 s3 == s4 是 true。
而作为对比,如果我们将intern方法下移一行,那么就会大不一样
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
明显,我们这里不过多的像上面一样的分析,字符串常量池中没有s3对象,s4是字面值,在字符串常量池中直接创建“11”,所以二者一定不相等,结果是false。
字符串常量池(String pool)的位置
在jdk1.8之前,字符串常量池是在运行时常量池中,而运行常量池是方法区一部分,jdk1.8之前的版本,方法区是用永久代的概念来实现的,具体位置位于堆内存中。而
在 jdk1.6 以及以前的版本中,字符串的常量池是放在堆的 Perm 区的,Perm 区是一个类静态的区域,主要存储一些加载类的信息,常量池,方法片段等内容,默认大小只有4m。
在jdk1.7时,字符串常量池已经从 永久代区移到正常的 Java Heap 区域了。
jdk1.8及以后,采用元空间来实现,它们使用的是本地内存。
为什么要移动,永久代区域太小是一个主要原因。
字符串常量池一般是不进行垃圾回收的,因为字符串池存在的初衷就是为了提高效率,减少开销,尽量让常用的String对象能够直接使用。
所以jdk1.6的分析和上面以及下面的例子是完全不一样的,我们这里所有的例子都是基于jdk1.8之后的。
对于jdk1.6,分析时把握住永久代区域和堆内存不在一块,所以intern时,不能够放堆内存中的String对象的引用,所以得重新创建一个,所以两个对象是独立的。
相关面试题测试
上面讲了字符串常量池,我们对于String对象在java内存中的位置有了一些了解,下面就结合一些例子来分析分析,这些例子是可以作为一些简单的面试题。
例子1:字符串池中对于值相同的String对象最多存在一个,字面值创建
//采用字面值赋值
String s1 = "abc";//存在于字符串池中的对象
String s2 = "abc";//存在于字符串池中的对象
System.out.println(s1 == s2);//true
一开始,我们的字符串池为空,先在字符串池中创建“abc”这个String对象,再把这个对象返回给s1,然后在初始化s2时,发现字符串池中已经有了“abc”,那么此时直接把该对象返回给s2,所以这里我们的输出结果为true,即s1与s2指向的是同一对象。
例子2:new关键字实例化String对象
//采用new关键字创建一个字符串对象
String s3 = new String("abc");//存在于堆中的对象
String s4 = new String("abc");//存在于堆中的对象
System.out.println(s3 == s4);//false
一开始,我们的字符串池为空,初始化s3时,先在堆中new一个“abc”对象,然后查询字符串池发现没有该对象,于是放入,并将刚刚new的那个堆中的对象返回。然后在初始化s4时,同样,先又在堆中new一个“abc”对象,然后查询字符串池发现已经有了该对象,于是不操作,并将刚刚new的那个堆中的对象返回。因为s3和s4分别指向堆中的一个对象,所以肯定内存地址不相等。(字符串常量池中的“abc”与s3,s4也不相等哦,因为内存地址不同)
例子3:字符串拼接
//当字符串池中有abc时,true
String s5 = "abc" + "def";//编译时就已经确定,这是从字符串池中返回的对象
String s6 = "abcdef";//存在于字符串池中的对象
String s7 = new String("abc") + new String("def");//运行时才生成,在堆里
System.out.println(s5 == s6);//true
System.out.println(s6 == s7);//false
同样一开始字符串为空,我们的s5是两个字面字符串的拼接,这里因为是字面值,所以编译期编译器就直接计算了拼接结果,然后放入了字符串池,所以这里就相当于String s5 = "abcdef"
。
而s7是两个String对象的拼接,在编译期编译器并不能识别他们拼接的结果,所以这里是在运行期才完成的拼接,这里先在堆中创建了“abc”对象,又在堆中创建了”def“对象,又因为String对象是不能更改的,所以是在堆中创建了一个新的字符串来返回给s7。
所以最终s5==s6,而s7!=s6。
总之一句话:字面字符串拼接编译期,而引用字符串拼接运行期。
例子4:String对象字符串拼接,jdk9和之前版本区别,StringBuilder
String s1 = "abc";
String s2 = "def";
String s3 = s1 + s2;
我们来看这样一段代码。针对这段代码,网上很多博客与教学视频都会如此解释:这里s3这行代码,其实是先实例化了一个StringBuilder对象,然后调用其append方法,分别将s1和s2的对象值拼接进来,然后继续链式调用tostring方法,返回一个字符串给s3,所以这里返回的对象实际是在堆内存中。
这种说法是没错的,我们看其字节码文件也能看出。
但实际上,这是在jdk9以前的jdk版本的情况。
如果我们使用的是jdk9的话,就不是这么个道理了。使用jdk9,不会再实例化StringBuilder对象,而是因为动态调用,返回一个String对象。当然这里还是在堆中。我们通过下面的字节码指令来证明。
这里用到了invokeDynamic指令。
例子5:intern方法
String s1 = new String("abc");
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s2==s3);//true
String.intern()方法,是主动将该String对象放入字符串池中,如果字符串中已经存在,则不操作,并返回字符串池中的对象,如果不存在,则放入字符串池中,并返回字符串池中的对象。
这里注意无论怎样,返回的都是字符串池中的对象。
所以s1的对象是在堆内存里new的,而s2的是字符串池返回的,s3也是字符串池返回的,所以最后结果是true。
总结
下面简短几句话来总结,便于之后复习:
- 字面值在编译期就会放进常量池,或者查询常量池。
- 而new的话,不管怎样都要在堆中new对象,返回的也是堆中的对象,并且在常量池中保证有同值的字面值。
- 字面值的拼接相当于本身就是字面值。
- 而new 的String对象的拼接是在运行时才确定的,并且这个拼接的结果不会放进常量池中。
- intern方法放进常量池的是String对象,而不是字面值。