剖析new String()创建几个对象?及不同的方式创建的string对象有什么区别?

今天看到一个很有意思的高频面试题,其中的内容确确实实很有意思,下面我们来聊聊,先来看几道面试题,试试水平;

目录

面试题类型1:

面试题类型2:

 解释:

目前来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  首先回顾「字符串常量池」:

下面我们看看JVM是如何处理面试题类型一中的前两个代码的:

新知识引入:

使用+拼接字符串的实现原理


面试题类型1:

分别执行下面代码:请回答执行他们的过程中总共创建了几个字符串对象?

代码一:new String("xxx");//一个或两个

代码二:String str = "abc" + "def";//0个或一个

代码三:String s=new String(“xxx”);//一个或两个

代码四:String str = "abc" + new String("xxx");//5个

面试题类型2:

回答如果执行下列代码,分别返回的结果都是什么?

String s1 = "Java";
String s2 = "Java";
System.out.println(s1 == s2);//true
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s==s1);//false
String s1 = new String("javaer-wang");
String s2 = new String("javaer-wang");
System.out.println(s1 == s2);//false
String s1 = "abc";
String s2 = new String("def");
String s3 = s1 + s2;
String s4 = "abcdef";
System.out.println(s3==s4); //false

 解释:

接下来我们一边解释面试题,一边总结相关知识点;

目前来看,关于 new String("xxx") 创建对象个数的答案有 3 种:

  1. 有人说创建了 1 个对象;
  2. 有人说创建了 2 个对象;
  3. 有人说创建了 1 个或 2 个对象。

而出现多个答案的关键争议点在 【字符串常量池 】上,有的说 new 字符串的方式会在常量池创建一个字符串对象,有人说 new 字符串的时候并不会去字符串常量池创建对象,而是在调用 intern() 方法时,才会去字符串常量池检测并创建字符串,那么实际情况是怎样的呢?我们接下来慢慢聊。。。。

  首先回顾「字符串常量池」:

  • 字符串的分配和其他的对象分配一样,需要耗费高昂的时间和空间为代价,如果需要大量频繁的创建字符串,会极大程度地影响程序的性能,因此 JVM 为了提高性能和减少内存开销引入了字符串常量池(Constant Pool Table)的概念。
  • 字符串常量池相当于给字符串开辟一个常量池空间类似于缓存区,对于直接赋值的字符串(String s="xxx")来说,在每次创建字符串时优先使用已经存在字符串常量池的字符串,如果字符串常量池没有相关的字符串,会先在字符串常量池中创建该字符串,然后将引用地址返回变量,如下图:


下面我们看看JVM是如何处理面试题类型一中的前两个代码的:

JVM如何执行String s="abc";
        JVM在执行String s="abc"时,会先在常量池中查找是否存在"abc"这个字符串,如果存在,则将s1指向该字符串;如果不存在,则在常量池中创建一个新的字符串"abc",然后将s指向该字符串。如果在后续的代码中使用了相同的字符串字面量,JVM会重用之前创建的String对象,而不是创建新的对象。

JVM如何执行new String("abc");
        当执行 new String("abc") 时,JVM 会首先在常量池中查找是否已经存在 "abc" 这个字符串,如果存在,则直接返回该字符串的引用;如果不存在,则在堆中创建一个新的 String 对象,并将 "abc" 这个字符串的引用赋值给该对象。同时,该对象会被添加到常量池中,以便下次使用时直接返回该对象的引用。

        你也可以这样理解;new String("abc")相当于new String(String s1="abc"),即先要执行String s1="abc",然后再在堆区new一个String对象。

因此,现在可以解答本文的标题了,String s=new String("abc")创建了1或2个对象,String s="abc"创建了0或1个对象。

我在博客上看到一位博主对以上执行过程分析为:

String s="abc"会先从字符串常量池(下文简称常量池)中查找,如果常量池中已经存在"abc",而"abc"必定指向堆区的某个String对象,那么直接将s指向这个String对象即可;

我并不认可这句话(仅仅代表我的观点),下面我们用例子来证明我的观点; 

按照以上说法;下列代码返回的值应该为true,我们实际执行看结果;

String s2="abc";
String s1=new String("abc");
String s3=new String("abc");
System.out.println(s1==s2);//false
System.out.println(s1==s3);//false

 我们再次通过一个例子更加贴切的理解一下该过程(我们在看一个例子):

底层原理图与原理我们用画图来解释;

 

 String类型的数据都存放在常量池,堆中的属性只存放常量池中值的地址!

①    String str1 = "abc";语句是直接创建了字符串,是构造字符串最常用的方式。这种直接赋值的方式,并没有用new关键字在堆中开辟新的地址,而是在常量池中开辟了地址。所以引用str1的内容是常量池中abc字符串的地址0x01。

②   String str2 = "abc";也是直接创建了字符串。但是因为String是特殊的引用类型,其存放在常量池的内容是不可变的。在创建一个字符串对象时,会先在常量池中查询是否已存在,若不存在则再开辟新的存储空间。此时str2所引用的对象abc和str1是一样的,所以这是常量池中已经存在的内容。Str2会直接引用常量池中这个已经存在的字符串对象。所以str2的内容也是常量池中abc字符串的地址0x01。

③    String str3 = new String("abc");语句用new在堆中开辟了空间,是通过创建对象来创建字符串的。str3指向堆中一个新的空间的地址0x02,此空间中存放String对象的属性值。同样的,此属性值在赋值为"abc"时,会先在常量池中查询是否已存在此内容。此时常量池中已经存在了abc,所以属性值存储的是常量池中abc的地址0x01

④    String str4 = new String("abc");语句也是用nuw在堆中开辟了空间,利用创建新对象来创建字符串。str4指向堆中一个新的空间的地址0x03,此空间中存放String对象的属性值。同样的,此属性值在赋值为"abc"时,会先在常量池中查询是否已存在此内容。此时常量池中已经存在了abc,所以属性值存储的是常量池中abc的地址0x01。

⑤    如果通过创建新的对象来创建字符串,发现常量池中没有已存在的字符串;那么JVM会继续在常量池中开辟一个新的空间并把值存放在其中,而堆中的属性值为此空间的地址。

以上案例来源:https://www.baidu.com/link?url=xlIOX3u9Gv11cc6XlDpWjEIMaDrhaWtKinWiDEle_GNNz-OVvqntCjlr2o9FDVbj&wd=&eqid=a5c4a04300005cce00000003641ad2c6


然后我们来解释一下一下代码:

代码二:String str = "abc" + "def";//0个或一个

代码四:String str = "abc" + new String("xxx");//5个

        对于代码二中,如果你不知道他的底层实现,你估计会首先想到String str = "abc" + "def"执行过程中首先创建了两个对象“abc”和“def”然后在进行拼接最后构成第三个对象str;然而实际并不是这样,对于我们的JVM频繁的创建对象是很浪费内存和时间的,那么JVM是如何处理使用“+”进行的字符串拼接的呢?

        底层实现:用+操作符拼接字符串,会产生一个中间对象,如果是线程安全的环境,我们会用StringBuffer拼接字符串,线程不安全的环境则使用StringBuilder。

        总结:上面的问题涉及到字符串常量重载“+”的问题,当一个字符串由多个字符串常量拼接成一个字符串时,它自己也肯定是字符串常量。字符串常量的“+”号连接Java虚拟机会在程序编译期将其优化为连接后的值。就上面的示例而言,在编译时已经被合并成“abcdef”字符串,因此,只会创建1个对象(你也可以说是两个,因为还可以加上stringbuffer或stringbuilder)。并没有创建临时字符串对象abc和def,这样减轻了垃圾收集器的压力 ;

引入一个知识点:

        JVM在编译我们自己写的代码时,会将我们写的代码进行优化      

理解了上述例子,那么第四个例子也就好理解了:

        上述的代码Java虚拟机在编译的时候同样会优化,会创建一个StringBuilder来进行字符串的拼接,实际效果类似:

String s = new String("def");
new StringBuilder().append("abc").append(s).toString();

很显然,多出了一个StringBuilder对象,那就应该是5个对象。

        此时,你也或许有这样的疑问,StringBuilder最后toString()之后的“abcdef”难道不在常量池存一份吗?这个还真没有存,我们来看一下这段代码:

    String s1 = "abc";
    String s2 = new String("def");
    String s3 = s1 + s2;
    String s4 = "abcdef";
    System.out.println(s3==s4); // false

        按照上面的分析,如果s1+s2的结果在常量池中存了一份,那么s3中的value引用应该和s4中value的引用是一样的才对。但是结果却大相径庭,他并没有存到常量池中而是存到了堆内存中,上述代码会证明此观点;

为什么会这样呢?我们记以下概念:

仅有使用引号包含文本的方式创建的string对象之间使用“+”连接的新对象才会被加入到字符串常量池中,而对于包含new string()或null“+”字符串的连接方式产生的新对象不会被添加到字符串常量池中;

验证上述概念:

        /*
        String s5="qwe"+"asd";
        String s6="qweasd";
        System.out.println(s5==s6);//true
        */
        String s5="qwe"+new String("asd");
        String s6="qweasd";
        System.out.println(s5==s6);//false

继续总结:

String的两种初始化形式是有本质区别的。

String str1 = "abc";  // 在常量池中

String str2 = new String("abc"); // 在堆上

新知识引入:

使用+拼接字符串的实现原理

        前面提到过,使用+拼接字符串,其实只是Java提供的一个语法糖,看看他的内部原理到底是如何实现的。还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。

String a = "Hello";
String b = "world";
String c = a + "," + b;

反编译后的内容如下:

String a = "Hello";
String b = "world";
String c = (new StringBuilder()).append(a).append(",").append(b).toString();

        通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。也就是说 +和StringBuilder的append等价,

也就是说使用+拼接字符串的实现原理就是使用StringBuilder.append。

其实偶尔看看Java知识点的底层实现也是很有意思的,希望大家在学习中能够拥有刨根问底的学习精神; 

我很清楚自己想要什么,想成为什么,该怎么做,如何做。但我无法打败自己,所以迄今为止我还是我;

                                                                ——

猜你喜欢

转载自blog.csdn.net/m0_64231944/article/details/129714801
今日推荐