java中String的引用问题 -java基础复习部分

本篇博客是我在复习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方法下移一行,那么就会大不一样

扫描二维码关注公众号,回复: 12618661 查看本文章
	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对象,而不是字面值。

参考文献

美团技术文章:深入解析String#intern
我自己之前的博客:jvm学习 java字符串常量池以及String常见简单面试问题

猜你喜欢

转载自blog.csdn.net/qq_34687559/article/details/112507110